co.easimart.Easimart Maven / Gradle / Ivy
package co.easimart;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.util.Log;
import co.easimart.http.EasimartNetworkInterceptor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import bolts.Continuation;
import bolts.Task;
/**
* The {@code Easimart} class contains static functions that handle global configuration for the Easimart
* library.
*/
public class Easimart {
private static final String EASIMART_APPLICATION_ID = "co.easimart.APPLICATION_ID";
private static final String EASIMART_CLIENT_KEY = "co.easimart.CLIENT_KEY";
private static final Object MUTEX = new Object();
/* package */ static EasimartEventuallyQueue eventuallyQueue = null;
//region LDS
private static boolean isLocalDatastoreEnabled;
private static OfflineStore offlineStore;
/**
* Enable pinning in your application. This must be called before your application can use
* pinning. You must invoke {@code enableLocalDatastore(Context)} before
* {@link #initialize(Context)} :
*
*
* public class MyApplication extends Application {
* public void onCreate() {
* Easimart.enableLocalDatastore(this);
* Easimart.initialize(this);
* }
* }
*
*
* @param context
* The active {@link Context} for your application.
*/
public static void enableLocalDatastore(Context context) {
if (isInitialized()) {
throw new IllegalStateException("`Easimart#enableLocalDatastore(Context)` must be invoked " +
"before `Easimart#initialize(Context)`");
}
isLocalDatastoreEnabled = true;
}
/* package for tests */ static void disableLocalDatastore() {
setLocalDatastore(null);
// We need to re-register EasimartCurrentInstallationController otherwise it is still offline
// controller
EasimartCorePlugins.getInstance().reset();
}
/* package */ static OfflineStore getLocalDatastore() {
return offlineStore;
}
/* package for tests */ static void setLocalDatastore(OfflineStore offlineStore) {
Easimart.isLocalDatastoreEnabled = offlineStore != null;
Easimart.offlineStore = offlineStore;
}
/* package */ static boolean isLocalDatastoreEnabled() {
return isLocalDatastoreEnabled;
}
//endregion
/**
* Authenticates this client as belonging to your application.
*
* You must define {@code co.easimart.APPLICATION_ID} and {@code co.easimart.CLIENT_KEY}
* {@code meta-data} in your {@code AndroidManifest.xml}:
*
* <manifest ...>
*
* ...
*
* <application ...>
* <meta-data
* android:name="co.easimart.APPLICATION_ID"
* android:value="@string/parse_app_id" />
* <meta-data
* android:name="co.easimart.CLIENT_KEY"
* android:value="@string/parse_client_key" />
*
* ...
*
* </application>
* </manifest>
*
*
* This must be called before your application can use the Easimart library.
* The recommended way is to put a call to {@code Easimart.initialize}
* in your {@code Application}'s {@code onCreate} method:
*
*
* public class MyApplication extends Application {
* public void onCreate() {
* Easimart.initialize(this);
* }
* }
*
*
* @param context
* The active {@link Context} for your application.
*/
public static void initialize(Context context) {
Context applicationContext = context.getApplicationContext();
String applicationId;
String clientKey;
Bundle metaData = ManifestInfo.getApplicationMetadata(applicationContext);
if (metaData != null) {
applicationId = metaData.getString(EASIMART_APPLICATION_ID);
clientKey = metaData.getString(EASIMART_CLIENT_KEY);
if (applicationId == null) {
throw new RuntimeException("ApplicationId not defined. " +
"You must provide ApplicationId in AndroidManifest.xml.\n" +
"\" />");
}
if (clientKey == null) {
throw new RuntimeException("ClientKey not defined. " +
"You must provide ClientKey in AndroidManifest.xml.\n" +
"\" />");
}
} else {
throw new RuntimeException("Can't get Application Metadata");
}
initialize(context, applicationId, clientKey);
}
/**
* Authenticates this client as belonging to your application.
*
* This method is only required if you intend to use a different {@code applicationId} or
* {@code clientKey} than is defined by {@code co.easimart.APPLICATION_ID} or
* {@code co.easimart.CLIENT_KEY} in your {@code AndroidManifest.xml}.
*
* This must be called before your
* application can use the Easimart library. The recommended way is to put a call to
* {@code Easimart.initialize} in your {@code Application}'s {@code onCreate} method:
*
*
* public class MyApplication extends Application {
* public void onCreate() {
* Easimart.initialize(this, "your application id", "your client key");
* }
* }
*
*
* @param context
* The active {@link Context} for your application.
* @param applicationId
* The application id provided in the Easimart dashboard.
* @param clientKey
* The client key provided in the Easimart dashboard.
*/
public static void initialize(Context context, String applicationId, String clientKey) {
EasimartPlugins.Android.initialize(context, applicationId, clientKey);
Context applicationContext = context.getApplicationContext();
EasimartHttpClient.setKeepAlive(true);
EasimartHttpClient.setMaxConnections(20);
// If we have interceptors in list, we have to initialize all http clients and add interceptors
if (interceptors != null) {
initializeEasimartHttpClientsWithEasimartNetworkInterceptors();
}
EasimartObject.registerEasimartSubclasses();
if (isLocalDatastoreEnabled()) {
offlineStore = new OfflineStore(context);
} else {
EasimartKeyValueCache.initialize(context);
}
// Make sure the data on disk for Easimart is for the current
// application.
checkCacheApplicationId();
new Thread("Easimart.initialize Disk Check & Starting Command Cache") {
@Override
public void run() {
// Trigger the command cache to flush its contents.
getEventuallyQueue();
}
}.start();
EasimartFieldOperations.registerDefaultDecoders();
if (!allEasimartPushIntentReceiversInternal()) {
throw new SecurityException("To prevent external tampering to your app's notifications, " +
"all receivers registered to handle the following actions must have " +
"their exported attributes set to false: co.easimart.push.intent.RECEIVE, "+
"co.easimart.push.intent.OPEN, co.easimart.push.intent.DELETE");
}
// May need to update GCM registration ID if app version has changed.
// This also primes current installation.
GcmRegistrar.getInstance().registerAsync().continueWithTask(new Continuation>() {
@Override
public Task then(Task task) throws Exception {
// Prime current user in the background
return EasimartUser.getCurrentUserAsync().makeVoid();
}
}).continueWith(new Continuation() {
@Override
public Void then(Task task) throws Exception {
// Prime config in the background
EasimartConfig.getCurrentConfig();
return null;
}
}, Task.BACKGROUND_EXECUTOR);
if (ManifestInfo.getPushType() == PushType.PPNS) {
PushService.startServiceIfRequired(applicationContext);
}
dispatchOnEasimartInitialized();
// FYI we probably don't want to do this if we ever add other callbacks.
synchronized (MUTEX_CALLBACKS) {
Easimart.callbacks = null;
}
}
/* package */ static void destroy() {
EasimartEventuallyQueue queue;
synchronized (MUTEX) {
queue = eventuallyQueue;
eventuallyQueue = null;
}
if (queue != null) {
queue.onDestroy();
}
EasimartCorePlugins.getInstance().reset();
EasimartPlugins.reset();
}
/**
* @return {@code True} if {@link #initialize} has been called, otherwise {@code false}.
*/
/* package */ static boolean isInitialized() {
return EasimartPlugins.get() != null;
}
static Context getApplicationContext() {
checkContext();
return EasimartPlugins.Android.get().applicationContext();
}
/**
* Checks that each of the receivers associated with the three actions defined in
* EasimartPushBroadcastReceiver (ACTION_PUSH_RECEIVE, ACTION_PUSH_OPEN, ACTION_PUSH_DELETE) has
* their exported attributes set to false. If this is the case for each of the receivers
* registered in the AndroidManifest.xml or if no receivers are registered (because we will be registering
* the default implementation of EasimartPushBroadcastReceiver in PushService) then true is returned.
* Note: the reason for iterating through lists, is because you can define different receivers
* in the manifest that respond to the same intents and both all of the receivers will be triggered.
* So we want to make sure all them have the exported attribute set to false.
*/
private static boolean allEasimartPushIntentReceiversInternal() {
List intentReceivers = ManifestInfo.getIntentReceivers(
EasimartPushBroadcastReceiver.ACTION_PUSH_RECEIVE,
EasimartPushBroadcastReceiver.ACTION_PUSH_DELETE,
EasimartPushBroadcastReceiver.ACTION_PUSH_OPEN);
for (ResolveInfo resolveInfo : intentReceivers) {
if (resolveInfo.activityInfo.exported) {
return false;
}
}
return true;
}
/**
* @deprecated Please use {@link #getEasimartCacheDir(String)} or {@link #getEasimartFilesDir(String)}
* instead.
*/
@Deprecated
/* package */ static File getEasimartDir() {
return EasimartPlugins.get().getEasimartDir();
}
/* package */ static File getEasimartCacheDir() {
return EasimartPlugins.get().getCacheDir();
}
/* package */ static File getEasimartCacheDir(String subDir) {
synchronized (MUTEX) {
File dir = new File(getEasimartCacheDir(), subDir);
if (!dir.exists()) {
dir.mkdirs();
}
return dir;
}
}
/* package */ static File getEasimartFilesDir() {
return EasimartPlugins.get().getFilesDir();
}
/* package */ static File getEasimartFilesDir(String subDir) {
synchronized (MUTEX) {
File dir = new File(getEasimartFilesDir(), subDir);
if (!dir.exists()) {
dir.mkdirs();
}
return dir;
}
}
/**
* Verifies that the data stored on disk for Easimart was generated using the same application that
* is running now.
*/
static void checkCacheApplicationId() {
synchronized (MUTEX) {
String applicationId = EasimartPlugins.get().applicationId();
if (applicationId != null) {
File dir = Easimart.getEasimartCacheDir();
// Make sure the current version of the cache is for this application id.
File applicationIdFile = new File(dir, "applicationId");
if (applicationIdFile.exists()) {
// Read the file
boolean matches = false;
try {
RandomAccessFile f = new RandomAccessFile(applicationIdFile, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
String diskApplicationId = new String(bytes, "UTF-8");
matches = diskApplicationId.equals(applicationId);
} catch (FileNotFoundException e) {
// Well, it existed a minute ago. Let's assume it doesn't match.
} catch (IOException e) {
// Hmm, the applicationId file was malformed or something. Assume it
// doesn't match.
}
// The application id has changed, so everything on disk is invalid.
if (!matches) {
try {
EasimartFileUtils.deleteDirectory(dir);
} catch (IOException e) {
// We're unable to delete the directy...
}
}
}
// Create the version file if needed.
applicationIdFile = new File(dir, "applicationId");
try {
FileOutputStream out = new FileOutputStream(applicationIdFile);
out.write(applicationId.getBytes("UTF-8"));
out.close();
} catch (FileNotFoundException e) {
// Nothing we can really do about it.
} catch (UnsupportedEncodingException e) {
// Nothing we can really do about it. This would mean Java doesn't
// understand UTF-8, which is unlikely.
} catch (IOException e) {
// Nothing we can really do about it.
}
}
}
}
/**
* Gets the shared command cache object for all EasimartObjects. This command cache is used to
* locally store save commands created by the EasimartObject.saveEventually(). When a new
* EasimartCommandCache is instantiated, it will begin running its run loop, which will start by
* processing any commands already stored in the on-disk queue.
*/
/* package */ static EasimartEventuallyQueue getEventuallyQueue() {
Context context = EasimartPlugins.Android.get().applicationContext();
synchronized (MUTEX) {
boolean isLocalDatastoreEnabled = Easimart.isLocalDatastoreEnabled();
if (eventuallyQueue == null
|| (isLocalDatastoreEnabled && eventuallyQueue instanceof EasimartCommandCache)
|| (!isLocalDatastoreEnabled && eventuallyQueue instanceof EasimartPinningEventuallyQueue)) {
checkContext();
EasimartHttpClient httpClient = EasimartPlugins.get().restClient();
eventuallyQueue = isLocalDatastoreEnabled
? new EasimartPinningEventuallyQueue(context, httpClient)
: new EasimartCommandCache(context, httpClient);
// We still need to clear out the old command cache even if we're using Pinning in case
// anything is left over when the user upgraded. Checking number of pending and then
// initializing should be enough.
if (isLocalDatastoreEnabled && EasimartCommandCache.getPendingCount() > 0) {
new EasimartCommandCache(context, httpClient);
}
}
return eventuallyQueue;
}
}
static void checkInit() {
if (EasimartPlugins.get() == null) {
throw new RuntimeException("You must call Easimart.initialize(Context)"
+ " before using the Easimart library.");
}
if (EasimartPlugins.get().applicationId() == null) {
throw new RuntimeException("applicationId is null. "
+ "You must call Easimart.initialize(Context)"
+ " before using the Easimart library.");
}
if (EasimartPlugins.get().clientKey() == null) {
throw new RuntimeException("clientKey is null. "
+ "You must call Easimart.initialize(Context)"
+ " before using the Easimart library.");
}
}
static void checkContext() {
if (EasimartPlugins.Android.get().applicationContext() == null) {
throw new RuntimeException("applicationContext is null. "
+ "You must call Easimart.initialize(Context)"
+ " before using the Easimart library.");
}
}
static boolean hasPermission(String permission) {
return (getApplicationContext().checkCallingOrSelfPermission(permission) ==
PackageManager.PERMISSION_GRANTED);
}
static void requirePermission(String permission) {
if (!hasPermission(permission)) {
throw new IllegalStateException(
"To use this functionality, add this to your AndroidManifest.xml:\n"
+ " ");
}
}
//region EasimartCallbacks
private static final Object MUTEX_CALLBACKS = new Object();
private static Set callbacks = new HashSet<>();
/**
* Registers a listener to be called at the completion of {@link #initialize}.
*
* Throws {@link java.lang.IllegalStateException} if called after {@link #initialize}.
*
* @param listener the listener to register
*/
/* package */ static void registerEasimartCallbacks(EasimartCallbacks listener) {
if (isInitialized()) {
throw new IllegalStateException(
"You must register callbacks before Easimart.initialize(Context)");
}
synchronized (MUTEX_CALLBACKS) {
if (callbacks == null) {
return;
}
callbacks.add(listener);
}
}
/**
* Unregisters a listener previously registered with {@link #registerEasimartCallbacks}.
*
* @param listener the listener to register
*/
/* package */ static void unregisterEasimartCallbacks(EasimartCallbacks listener) {
synchronized (MUTEX_CALLBACKS) {
if (callbacks == null) {
return;
}
callbacks.remove(listener);
}
}
private static void dispatchOnEasimartInitialized() {
EasimartCallbacks[] callbacks = collectEasimartCallbacks();
if (callbacks != null) {
for (EasimartCallbacks callback : callbacks) {
callback.onEasimartInitialized();
}
}
}
private static EasimartCallbacks[] collectEasimartCallbacks() {
EasimartCallbacks[] callbacks;
synchronized (MUTEX_CALLBACKS) {
if (Easimart.callbacks == null) {
return null;
}
callbacks = new EasimartCallbacks[Easimart.callbacks.size()];
if (Easimart.callbacks.size() > 0) {
callbacks = Easimart.callbacks.toArray(callbacks);
}
}
return callbacks;
}
/* package */ interface EasimartCallbacks {
public void onEasimartInitialized();
}
//endregion
//region Logging
public static final int LOG_LEVEL_VERBOSE = Log.VERBOSE;
public static final int LOG_LEVEL_DEBUG = Log.DEBUG;
public static final int LOG_LEVEL_INFO = Log.INFO;
public static final int LOG_LEVEL_WARNING = Log.WARN;
public static final int LOG_LEVEL_ERROR = Log.ERROR;
public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE;
/**
* Sets the level of logging to display, where each level includes all those below it. The default
* level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link #LOG_LEVEL_ERROR}
* or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is
* logged. The levels are:
*
* - {@link #LOG_LEVEL_VERBOSE}
* - {@link #LOG_LEVEL_DEBUG}
* - {@link #LOG_LEVEL_INFO}
* - {@link #LOG_LEVEL_WARNING}
* - {@link #LOG_LEVEL_ERROR}
* - {@link #LOG_LEVEL_NONE}
*
*
* @param logLevel
* The level of logcat logging that Easimart should do.
*/
public static void setLogLevel(int logLevel) {
EasimartLog.setLogLevel(logLevel);
}
/**
* Returns the level of logging that will be displayed.
*/
public static int getLogLevel() {
return EasimartLog.getLogLevel();
}
//endregion
// Suppress constructor to prevent subclassing
private Easimart() {
throw new AssertionError();
}
private static List interceptors;
// Initialize all necessary http clients and add interceptors to these http clients
private static void initializeEasimartHttpClientsWithEasimartNetworkInterceptors() {
// This means developers have not called addInterceptor method so we should do nothing.
if (interceptors == null) {
return;
}
List clients = new ArrayList<>();
// Rest http client
clients.add(EasimartPlugins.get().restClient());
// AWS http client
clients.add(EasimartCorePlugins.getInstance().getFileController().awsClient());
// Add interceptors to http clients
for (EasimartHttpClient easimartHttpClient : clients) {
// We need to add the decompress interceptor before the external interceptors to return
// a decompressed response to Easimart.
easimartHttpClient.addInternalInterceptor(new EasimartDecompressInterceptor());
for (EasimartNetworkInterceptor interceptor : interceptors) {
easimartHttpClient.addExternalInterceptor(interceptor);
}
}
// Remove interceptors reference since we do not need it anymore
interceptors = null;
}
/**
* Add a {@link EasimartNetworkInterceptor}. You must invoke
* {@code addEasimartNetworkInterceptor(EasimartNetworkInterceptor)} before
* {@link #initialize(Context)}. You can add multiple {@link EasimartNetworkInterceptor}.
*
* @param interceptor
* {@link EasimartNetworkInterceptor} to be added.
*/
public static void addEasimartNetworkInterceptor(EasimartNetworkInterceptor interceptor) {
if (isInitialized()) {
throw new IllegalStateException("`Easimart#addEasimartNetworkInterceptor(EasimartNetworkInterceptor)`"
+ " must be invoked before `Easimart#initialize(Context)`");
}
if (interceptors == null) {
interceptors = new ArrayList<>();
}
interceptors.add(interceptor);
}
/**
* Remove a given {@link EasimartNetworkInterceptor}. You must invoke
* {@code removeEasimartNetworkInterceptor(EasimartNetworkInterceptor)} before
* {@link #initialize(Context)}.
*
* @param interceptor
* {@link EasimartNetworkInterceptor} to be removed.
*/
public static void removeEasimartNetworkInterceptor(EasimartNetworkInterceptor interceptor) {
if (isInitialized()) {
throw new IllegalStateException("`Easimart#addEasimartNetworkInterceptor(EasimartNetworkInterceptor)`"
+ " must be invoked before `Easimart#initialize(Context)`");
}
if (interceptors == null) {
return;
}
interceptors.remove(interceptor);
}
/* package */ static String externalVersionName() {
return "a" + EasimartObject.VERSION_NAME;
}
}