package net.moddity.droidnubekit; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import net.moddity.droidnubekit.annotations.RecordType; import net.moddity.droidnubekit.errors.DNKErrorHandler; import net.moddity.droidnubekit.errors.DNKException; import net.moddity.droidnubekit.interfaces.CloudKitService; import net.moddity.droidnubekit.interfaces.CloudKitWebViewRedirectHandler; import net.moddity.droidnubekit.interfaces.DNKCloudKitAuth; import net.moddity.droidnubekit.objects.DNKObject; import net.moddity.droidnubekit.requests.DNKCallback; import net.moddity.droidnubekit.requests.DNKObjectProcessingCallback; import net.moddity.droidnubekit.requests.DNKRecordLookupRequest; import net.moddity.droidnubekit.requests.DNKRecordModifyRequest; import net.moddity.droidnubekit.requests.DNKRecordQueryRequest; import net.moddity.droidnubekit.responsemodels.DNKRecord; import net.moddity.droidnubekit.responsemodels.DNKRecordField; import net.moddity.droidnubekit.responsemodels.DNKRecordsResponse; import net.moddity.droidnubekit.responsemodels.DNKUser; import net.moddity.droidnubekit.responsemodels.DNKZone; import net.moddity.droidnubekit.ui.DNKWebViewAuthActivity; import net.moddity.droidnubekit.utils.DNKOperationType; import net.moddity.droidnubekit.utils.DNKRecordFieldDeserializer; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import dalvik.system.DexFile; import retrofit.Callback; import retrofit.RequestInterceptor; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.client.Response; import retrofit.converter.GsonConverter; /** * Created by Jaume Cornadó on 11/6/15. */ public class DroidNubeKit implements CloudKitWebViewRedirectHandler { /** The singleton instance */ private static DroidNubeKit instance; /** The CloudKit Service reference */ private CloudKitService cloudKitService; /** The API Token */ private String apiToken; /** Session on auth */ private String ckSession; /** If it's production or development */ private DroidNubeKitConstants.kEnvironmentType environmentType; private String appContainerIdentifier; private Context context; /** External redirect handler to control authentication */ private DNKCloudKitAuth cloudKitAuthHandler; private DNKUser currentUser; public Set<Class<?>> modelClasses = new HashSet<>(); /** * Initializes the CloudKit Service */ private DroidNubeKit() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(new TypeToken<Map<String, DNKRecordField>>() {}.getType(), new DNKRecordFieldDeserializer()); RequestInterceptor requestInterceptor = new RequestInterceptor() { @Override public void intercept(RequestInterceptor.RequestFacade request) { if(ckSession != null) request.addQueryParam("ckSession", ckSession); } }; RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(DroidNubeKitConstants.API_ENDPOINT) .setLogLevel(RestAdapter.LogLevel.FULL) .setConverter(new GsonConverter(gsonBuilder.create())) .setRequestInterceptor(requestInterceptor) .setErrorHandler(new DNKErrorHandler()) .build(); cloudKitService = restAdapter.create(CloudKitService.class); } /** * Singleton instance * @return the instance */ public static DroidNubeKit getInstance() { if(instance == null) instance = new DroidNubeKit(); return instance; } //---------------------- // Public methods //---------------------- /** * The method to initialize the library * @param apiToken Api Token obtained on CloudKit dashboard * @param appContainerIdentifier Your App Container identifier iCloud.net.moddity.yourapp (similar to this) * @param environmentType development / production environment * @param context Pass a context to the lib */ public static void initNube(String apiToken, String appContainerIdentifier, DroidNubeKitConstants.kEnvironmentType environmentType, Context context) { DroidNubeKit.getInstance().apiToken = apiToken; DroidNubeKit.getInstance().environmentType = environmentType; DroidNubeKit.getInstance().appContainerIdentifier = appContainerIdentifier; DroidNubeKit.getInstance().context = context; DroidNubeKit.getInstance().checkForSession(); try { DroidNubeKit.getInstance().modelClasses = DroidNubeKit.getInstance().getClasspathClasses(); } catch (Exception e) { //Todo throw inizialization exception e.printStackTrace(); } } /** * Fetch records using a query * @param queryRequest The query request object * @param environmentType development / production environment * @param callback callback to process the result */ public static <T> void fetchRecordsByQuery(DNKRecordQueryRequest queryRequest, DroidNubeKitConstants.kDatabaseType environmentType, final DNKCallback<List<T>> callback) { DroidNubeKit.getInstance().cloudKitService.queryRecords( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), environmentType.toString(), queryRequest, DroidNubeKit.getInstance().apiToken, new DNKObjectProcessingCallback<DNKRecordsResponse, T>() { @Override public void success(DNKRecordsResponse dnkRecordsResponse, Response response) { super.success(dnkRecordsResponse, response); callback.success(getResponseObjects()); } @Override public void failure(RetrofitError error) { super.failure(error); callback.failure(error.getCause()); } } ); } public static <T> void modifyRecord(T object, DNKOperationType operationType, DroidNubeKitConstants.kDatabaseType environmentType, final DNKCallback<List<T>> callback) { if(!(object instanceof DNKObject)) { callback.failure(new Exception("Object it's not instance of DNKRecord: " + object.toString())); return; } List<T> objects = new ArrayList<>(); objects.add(object); modifyRecord(objects, operationType, environmentType, callback); } /** * Modify a current record * @param objects The record to modify * @param operationType Operation type. More info at: https://developer.apple.com/library/prerelease/ios/documentation/DataManagement/Conceptual/CloutKitWebServicesReference * @param environmentType public / private * @param callback callback to process the result */ public static <T> void modifyRecord(List<T> objects, DNKOperationType operationType, DroidNubeKitConstants.kDatabaseType environmentType, final DNKCallback<List<T>> callback) { Map<String, DNKRecord> records = new HashMap<>(); for(T object : objects) { if (!(object instanceof DNKObject)) { callback.failure(new Exception("Object it's not instance of DNKRecord: " + object.toString())); return; } DNKObject recordObject = (DNKObject)object; records.put(recordObject.toRecord().getRecordName(), recordObject.toRecord()); for(DNKRecord descendingRecords : recordObject.getDescendingRecords()) { records.put(descendingRecords.getRecordName(), descendingRecords); } } DNKRecordModifyRequest request = DNKRecordModifyRequest.createRequest(new ArrayList<>(records.values()), operationType); DroidNubeKit.getInstance().cloudKitService.modifyRecords( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), environmentType.toString(), request, DroidNubeKit.getInstance().apiToken, new DNKObjectProcessingCallback<DNKRecordsResponse, T>() { @Override public void success(DNKRecordsResponse dnkRecordsResponse, Response response) { super.success(dnkRecordsResponse, response); callback.success(getResponseObjects()); } @Override public void failure(RetrofitError error) { super.failure(error); callback.failure(error.getCause()); } } ); } public static void getCurrentUser(final DNKCallback<DNKUser> callback) { DroidNubeKit.getInstance().cloudKitService.getCurrentUser( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), DroidNubeKit.getInstance().apiToken, new Callback<DNKUser>() { @Override public void success(DNKUser user, Response response) { callback.success(user); } @Override public void failure(RetrofitError error) { callback.failure(error.getCause()); } } ); } public static <T> void getObjects(List<T> objects, DroidNubeKitConstants.kDatabaseType databaseType, final DNKCallback<List<T>> callback) { if(objects == null || objects.size() == 0) return; List<DNKRecord> records = new ArrayList<>(); for(Object o : objects) { if (o instanceof DNKObject) { DNKObject dnkObject = (DNKObject)o; records.add(dnkObject.toRecord()); } } getRecords(records, databaseType, callback); } /** * Fetch multiple records from record objects * @param records * @param databaseType * @param callback */ public static <T> void getRecords(List<DNKRecord> records, DroidNubeKitConstants.kDatabaseType databaseType, final DNKCallback<List<T>> callback) { List<String> recordNames = new ArrayList<>(); for(DNKRecord record : records) { recordNames.add(record.getRecordName()); } getRecordByName(recordNames, databaseType, callback); } /** * Fetch multiple record by record name * @param recordNames * @param databaseType * @param callback */ public static <T> void getRecordByName(List<String> recordNames, DroidNubeKitConstants.kDatabaseType databaseType, final DNKCallback<List<T>> callback) { DNKRecordLookupRequest request = DNKRecordLookupRequest.createMultipleRecordRequest(recordNames); DroidNubeKit.getInstance().cloudKitService.lookupRecords( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), databaseType.toString(), request, DroidNubeKit.getInstance().apiToken, new DNKObjectProcessingCallback<DNKRecordsResponse, T>() { @Override public void success(DNKRecordsResponse dnkRecordsResponse, Response response) { super.success(dnkRecordsResponse, response); callback.success(getResponseObjects()); } @Override public void failure(RetrofitError error) { super.failure(error); callback.failure(error.getCause()); } } ); } /** * Fetch a single record by record name * @param recordName * @param databaseType * @param callback */ public static void getRecordByName(String recordName, DroidNubeKitConstants.kDatabaseType databaseType, final DNKCallback<DNKRecordsResponse> callback) { DNKRecordLookupRequest request = DNKRecordLookupRequest.createSingleRecordRequest(recordName); DroidNubeKit.getInstance().cloudKitService.lookupRecords( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), databaseType.toString(), request, DroidNubeKit.getInstance().apiToken, new Callback<DNKRecordsResponse>() { @Override public void success(DNKRecordsResponse dnkRecordsResponse, Response response) { callback.success(dnkRecordsResponse); } @Override public void failure(RetrofitError error) { callback.failure(error.getCause()); } } ); } /** * Fetches all the zones and their sync tokens in the specified database * @param environmentType development / production environment */ public static void getZones(DroidNubeKitConstants.kDatabaseType environmentType, final DNKCallback<List<DNKZone>> callback) { DroidNubeKit.getInstance().cloudKitService.getZones( DroidNubeKitConstants.PROTOCOL, DroidNubeKit.getInstance().appContainerIdentifier, DroidNubeKit.getInstance().environmentType.toString(), environmentType.toString(), DroidNubeKit.getInstance().apiToken, new Callback<List<DNKZone>>() { @Override public void success(List<DNKZone> zones, Response response) { callback.success(zones); } @Override public void failure(RetrofitError error) { callback.failure(error.getCause()); } } ); } /** * Gets the current context of the library * @return A reference to the context passed to the library */ public Context getContext() { return context; } /** * You must declare DNKWebViewAuthActivity on the AndroidManifest.xml of your application * @param redirectURL redirect url provided by CloudKit */ public static void showAuthDialog(String redirectURL) { Intent intent = new Intent(DroidNubeKit.getInstance().getContext(), DNKWebViewAuthActivity.class); intent.putExtra(DroidNubeKitConstants.WEBVIEW_REDIRECT_URL_EXTRA, redirectURL); intent.putExtra(DroidNubeKitConstants.WEBVIEW_REDIRECT_PATTERN_EXTRA, DroidNubeKitConstants.WEBVIEW_REDIRECT_URL_PREFIX+DroidNubeKit.getInstance().appContainerIdentifier.toLowerCase()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); DroidNubeKit.getInstance().getContext().startActivity(intent); } /** * Defines an Auth Handler interface to control the state of CloudKit authentication * @param authHandler */ public static void setCloudKitAuthHandler(DNKCloudKitAuth authHandler) { DroidNubeKit.getInstance().cloudKitAuthHandler = authHandler; } @Override public void onRedirectFound(Uri redirectUri) { if(DroidNubeKitConstants.WEBVIEW_REDIRECT_LOGIN_ENDPOINT.equals(redirectUri.getHost())) { String ckSession = redirectUri.getQueryParameter("ckSession"); if(ckSession != null) { saveckSession(ckSession); } } } private void checkForSession() { SharedPreferences sharedPreferences = getContext().getSharedPreferences(DroidNubeKitConstants.CLOUDKIT_SHARED_PREFERENCES, Context.MODE_PRIVATE); String ckSession = sharedPreferences.getString(DroidNubeKitConstants.CLOUDKIT_SESSION_KEY, ""); if(ckSession != null && ckSession.length() > 0) { DroidNubeKit.getInstance().ckSession = ckSession; checkSessionAlive(); } } private void saveckSession(String ckSession) { DroidNubeKit.getInstance().ckSession = ckSession; SharedPreferences sharedPreferences = getContext().getSharedPreferences(DroidNubeKitConstants.CLOUDKIT_SHARED_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(DroidNubeKitConstants.CLOUDKIT_SESSION_KEY, ckSession); editor.commit(); checkSessionAlive(); } private void checkSessionAlive() { getCurrentUser(new DNKCallback<DNKUser>() { @Override public void success(DNKUser dnkUser) { if (DroidNubeKit.getInstance().cloudKitAuthHandler != null) DroidNubeKit.getInstance().cloudKitAuthHandler.onAuthSucceed(); currentUser = dnkUser; } @Override public void failure(Throwable exception) { } }); } //http://bravenewgeek.com/implementing-spring-like-classpath-scanning-in-android/ private Set<Class<?>> getClasspathClasses() throws IOException, ClassNotFoundException { Set<Class<?>> classes = new HashSet<Class<?>>(); DexFile dex = new DexFile(getContext().getApplicationInfo().sourceDir); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Enumeration<String> entries = dex.entries(); while (entries.hasMoreElements()) { String entry = entries.nextElement(); if (entry.toLowerCase().startsWith(getContext().getPackageName().toLowerCase())) { Class<?> clazz = classLoader.loadClass(entry); if(clazz.isAnnotationPresent(RecordType.class)) { modelClasses.add(clazz); } classes.add(clazz); } } return classes; } }