/* * Copyright (c) 2015-present, Parse, LLC. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.parse; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import bolts.Continuation; import bolts.Task; import okhttp3.OkHttpClient; /** * The {@code Parse} class contains static functions that handle global configuration for the Parse * library. */ public class Parse { private static final String TAG = "com.parse.Parse"; private static final int DEFAULT_MAX_RETRIES = ParseRequest.DEFAULT_MAX_RETRIES; /** * Represents an opaque configuration for the {@code Parse} SDK configuration. */ public static final class Configuration { /** * Allows for simple constructing of a {@code Configuration} object. */ public static final class Builder { private Context context; private String applicationId; private String clientKey; private String server; private boolean localDataStoreEnabled; private OkHttpClient.Builder clientBuilder; private int maxRetries = DEFAULT_MAX_RETRIES; /** * Initialize a bulider with a given context. *

* This context will then be passed through to the rest of the Parse SDK for use during * initialization. *

*

* You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY} * {@code meta-data} in your {@code AndroidManifest.xml}: *

       * <manifest ...>
       *
       * ...
       *
       *   <application ...>
       *     <meta-data
       *       android:name="com.parse.SERVER_URL"
       *       android:value="@string/parse_server_url" />
       *     <meta-data
       *       android:name="com.parse.APPLICATION_ID"
       *       android:value="@string/parse_app_id" />
       *     <meta-data
       *       android:name="com.parse.CLIENT_KEY"
       *       android:value="@string/parse_client_key" />
       *
       *       ...
       *
       *   </application>
       * </manifest>
       * 
*

*

* This will cause the values for {@code server}, {@code applicationId} and {@code clientKey} to be set to * those defined in your manifest. * * @param context The active {@link Context} for your application. Cannot be null. */ public Builder(Context context) { this.context = context; // Yes, our public API states we cannot be null. But for unit tests, it's easier just to // support null here. if (context != null) { Context applicationContext = context.getApplicationContext(); Bundle metaData = ManifestInfo.getApplicationMetadata(applicationContext); if (metaData != null) { server(metaData.getString(PARSE_SERVER_URL)); applicationId = metaData.getString(PARSE_APPLICATION_ID); clientKey = metaData.getString(PARSE_CLIENT_KEY); } } } /** * Set the application id to be used by Parse. *

* This method is only required if you intend to use a different {@code applicationId} than * is defined by {@code com.parse.APPLICATION_ID} in your {@code AndroidManifest.xml}. * * @param applicationId The application id to set. * @return The same builder, for easy chaining. */ public Builder applicationId(String applicationId) { this.applicationId = applicationId; return this; } /** * Set the client key to be used by Parse. *

* This method is only required if you intend to use a different {@code clientKey} than * is defined by {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}. * * @param clientKey The client key to set. * @return The same builder, for easy chaining. */ public Builder clientKey(String clientKey) { this.clientKey = clientKey; return this; } /** * Set the server URL to be used by Parse. * * @param server The server URL to set. * @return The same builder, for easy chaining. */ public Builder server(String server) { // Add an extra trailing slash so that Parse REST commands include // the path as part of the server URL (i.e. http://api.myhost.com/parse) if (server != null && !server.endsWith("/")) { server = server + "/"; } this.server = server; return this; } /** * Enable pinning in your application. This must be called before your application can use * pinning. * * @return The same builder, for easy chaining. */ public Builder enableLocalDataStore() { localDataStoreEnabled = true; return this; } private Builder setLocalDatastoreEnabled(boolean enabled) { localDataStoreEnabled = enabled; return this; } /** * Set the {@link okhttp3.OkHttpClient.Builder} to use when communicating with the Parse * REST API *

* * @param builder The client builder, which will be modified for compatibility * @return The same builder, for easy chaining. */ public Builder clientBuilder(OkHttpClient.Builder builder) { clientBuilder = builder; return this; } /** * Set the max number of times to retry Parse operations before deeming them a failure *

* * @param maxRetries The maximum number of times to retry. <=0 to never retry commands * @return The same builder, for easy chaining. */ public Builder maxRetries(int maxRetries) { this.maxRetries = maxRetries; return this; } /** * Construct this builder into a concrete {@code Configuration} instance. * * @return A constructed {@code Configuration} object. */ public Configuration build() { return new Configuration(this); } } final Context context; final String applicationId; final String clientKey; final String server; final boolean localDataStoreEnabled; final OkHttpClient.Builder clientBuilder; final int maxRetries; private Configuration(Builder builder) { this.context = builder.context; this.applicationId = builder.applicationId; this.clientKey = builder.clientKey; this.server = builder.server; this.localDataStoreEnabled = builder.localDataStoreEnabled; this.clientBuilder = builder.clientBuilder; this.maxRetries = builder.maxRetries; } } private static final String PARSE_SERVER_URL = "com.parse.SERVER_URL"; private static final String PARSE_APPLICATION_ID = "com.parse.APPLICATION_ID"; private static final String PARSE_CLIENT_KEY = "com.parse.CLIENT_KEY"; private static final Object MUTEX = new Object(); static ParseEventuallyQueue 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() {
   *     Parse.enableLocalDatastore(this);
   *     Parse.initialize(this);
   *   }
   * }
   * 
* * @param context The active {@link Context} for your application. */ public static void enableLocalDatastore(Context context) { if (isInitialized()) { throw new IllegalStateException("`Parse#enableLocalDatastore(Context)` must be invoked " + "before `Parse#initialize(Context)`"); } isLocalDatastoreEnabled = true; } static void disableLocalDatastore() { setLocalDatastore(null); // We need to re-register ParseCurrentInstallationController otherwise it is still offline // controller ParseCorePlugins.getInstance().reset(); } static OfflineStore getLocalDatastore() { return offlineStore; } static void setLocalDatastore(OfflineStore offlineStore) { Parse.isLocalDatastoreEnabled = offlineStore != null; Parse.offlineStore = offlineStore; } public static boolean isLocalDatastoreEnabled() { return isLocalDatastoreEnabled; } //endregion /** * Authenticates this client as belonging to your application. *

* You may define {@code com.parse.SERVER_URL}, {@code com.parse.APPLICATION_ID} and (optional) {@code com.parse.CLIENT_KEY} * {@code meta-data} in your {@code AndroidManifest.xml}: *

   * <manifest ...>
   *
   * ...
   *
   *   <application ...>
   *     <meta-data
   *       android:name="com.parse.SERVER_URL"
   *       android:value="@string/parse_server_url" />
   *     <meta-data
   *       android:name="com.parse.APPLICATION_ID"
   *       android:value="@string/parse_app_id" />
   *     <meta-data
   *       android:name="com.parse.CLIENT_KEY"
   *       android:value="@string/parse_client_key" />
   *
   *       ...
   *
   *   </application>
   * </manifest>
   * 
*

* This must be called before your application can use the Parse library. * The recommended way is to put a call to {@code Parse.initialize} * in your {@code Application}'s {@code onCreate} method: *

*

   * public class MyApplication extends Application {
   *   public void onCreate() {
   *     Parse.initialize(this);
   *   }
   * }
   * 
* * @param context The active {@link Context} for your application. */ public static void initialize(Context context) { Configuration.Builder builder = new Configuration.Builder(context); if (builder.server == null) { throw new RuntimeException("ServerUrl not defined. " + "You must provide ServerUrl in AndroidManifest.xml.\n" + "\" />"); } if (builder.applicationId == null) { throw new RuntimeException("ApplicationId not defined. " + "You must provide ApplicationId in AndroidManifest.xml.\n" + "\" />"); } initialize(builder .setLocalDatastoreEnabled(isLocalDatastoreEnabled) .build() ); } /** * 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 com.parse.APPLICATION_ID} or * {@code com.parse.CLIENT_KEY} in your {@code AndroidManifest.xml}. *

* This must be called before your * application can use the Parse library. The recommended way is to put a call to * {@code Parse.initialize} in your {@code Application}'s {@code onCreate} method: *

*

   * public class MyApplication extends Application {
   *   public void onCreate() {
   *     Parse.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 Parse dashboard. * @param clientKey The client key provided in the Parse dashboard. */ public static void initialize(Context context, String applicationId, String clientKey) { initialize(new Configuration.Builder(context) .applicationId(applicationId) .clientKey(clientKey) .setLocalDatastoreEnabled(isLocalDatastoreEnabled) .build() ); } public static void initialize(Configuration configuration) { if (isInitialized()) { PLog.w(TAG, "Parse is already initialized"); return; } // NOTE (richardross): We will need this here, as ParsePlugins uses the return value of // isLocalDataStoreEnabled() to perform additional behavior. isLocalDatastoreEnabled = configuration.localDataStoreEnabled; ParsePlugins.initialize(configuration.context, configuration); try { ParseRESTCommand.server = new URL(configuration.server); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } ParseObject.registerParseSubclasses(); if (configuration.localDataStoreEnabled) { offlineStore = new OfflineStore(configuration.context); } else { ParseKeyValueCache.initialize(configuration.context); } // Make sure the data on disk for Parse is for the current // application. checkCacheApplicationId(); final Context context = configuration.context; Task.callInBackground(new Callable() { @Override public Void call() throws Exception { getEventuallyQueue(context); return null; } }); ParseFieldOperations.registerDefaultDecoders(); if (!allParsePushIntentReceiversInternal()) { 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: com.parse.push.intent.RECEIVE, " + "com.parse.push.intent.OPEN, com.parse.push.intent.DELETE"); } // May need to update GCM registration ID if app version has changed. // This also primes current installation. PushServiceUtils.initialize().continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // Prime current user in the background return ParseUser.getCurrentUserAsync().makeVoid(); } }).continueWith(new Continuation() { @Override public Void then(Task task) throws Exception { // Prime config in the background ParseConfig.getCurrentConfig(); return null; } }, Task.BACKGROUND_EXECUTOR); dispatchOnParseInitialized(); // FYI we probably don't want to do this if we ever add other callbacks. synchronized (MUTEX_CALLBACKS) { Parse.callbacks = null; } } static void destroy() { ParseEventuallyQueue queue; synchronized (MUTEX) { queue = eventuallyQueue; eventuallyQueue = null; } if (queue != null) { queue.onDestroy(); } ParseCorePlugins.getInstance().reset(); ParsePlugins.reset(); } /** * @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. */ static boolean isInitialized() { return ParsePlugins.get() != null; } static Context getApplicationContext() { checkContext(); return ParsePlugins.get().applicationContext(); } /** * Checks that each of the receivers associated with the three actions defined in * ParsePushBroadcastReceiver (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 ParsePushBroadcastReceiver 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 allParsePushIntentReceiversInternal() { List intentReceivers = ManifestInfo.getIntentReceivers( ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE, ParsePushBroadcastReceiver.ACTION_PUSH_DELETE, ParsePushBroadcastReceiver.ACTION_PUSH_OPEN); for (ResolveInfo resolveInfo : intentReceivers) { if (resolveInfo.activityInfo.exported) { return false; } } return true; } /** * @deprecated Please use {@link #getParseCacheDir(String)} or {@link #getParseFilesDir(String)} * instead. */ @Deprecated static File getParseDir() { return ParsePlugins.get().getParseDir(); } static File getParseCacheDir() { return ParsePlugins.get().getCacheDir(); } static File getParseCacheDir(String subDir) { synchronized (MUTEX) { File dir = new File(getParseCacheDir(), subDir); if (!dir.exists()) { dir.mkdirs(); } return dir; } } static File getParseFilesDir() { return ParsePlugins.get().getFilesDir(); } static File getParseFilesDir(String subDir) { synchronized (MUTEX) { File dir = new File(getParseFilesDir(), subDir); if (!dir.exists()) { dir.mkdirs(); } return dir; } } /** * Verifies that the data stored on disk for Parse was generated using the same application that * is running now. */ static void checkCacheApplicationId() { synchronized (MUTEX) { String applicationId = ParsePlugins.get().applicationId(); if (applicationId != null) { File dir = Parse.getParseCacheDir(); // 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 (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 { ParseFileUtils.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 (IOException e) { // Nothing we can really do about it. } } } } /** * Gets the shared command cache object for all ParseObjects. This command cache is used to * locally store save commands created by the ParseObject.saveEventually(). When a new * ParseCommandCache is instantiated, it will begin running its run loop, which will start by * processing any commands already stored in the on-disk queue. */ static ParseEventuallyQueue getEventuallyQueue() { Context context = ParsePlugins.get().applicationContext(); return getEventuallyQueue(context); } private static ParseEventuallyQueue getEventuallyQueue(Context context) { synchronized (MUTEX) { boolean isLocalDatastoreEnabled = Parse.isLocalDatastoreEnabled(); if (eventuallyQueue == null || (isLocalDatastoreEnabled && eventuallyQueue instanceof ParseCommandCache) || (!isLocalDatastoreEnabled && eventuallyQueue instanceof ParsePinningEventuallyQueue)) { checkContext(); ParseHttpClient httpClient = ParsePlugins.get().restClient(); eventuallyQueue = isLocalDatastoreEnabled ? new ParsePinningEventuallyQueue(context, httpClient) : new ParseCommandCache(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 && ParseCommandCache.getPendingCount() > 0) { new ParseCommandCache(context, httpClient); } } return eventuallyQueue; } } static void checkInit() { if (ParsePlugins.get() == null) { throw new RuntimeException("You must call Parse.initialize(Context)" + " before using the Parse library."); } if (ParsePlugins.get().applicationId() == null) { throw new RuntimeException("applicationId is null. " + "You must call Parse.initialize(Context)" + " before using the Parse library."); } } static void checkContext() { if (ParsePlugins.get().applicationContext() == null) { throw new RuntimeException("applicationContext is null. " + "You must call Parse.initialize(Context)" + " before using the Parse 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 ParseCallbacks 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 */ static void registerParseCallbacks(ParseCallbacks listener) { if (isInitialized()) { throw new IllegalStateException( "You must register callbacks before Parse.initialize(Context)"); } synchronized (MUTEX_CALLBACKS) { if (callbacks == null) { return; } callbacks.add(listener); } } /** * Unregisters a listener previously registered with {@link #registerParseCallbacks}. * * @param listener the listener to register */ static void unregisterParseCallbacks(ParseCallbacks listener) { synchronized (MUTEX_CALLBACKS) { if (callbacks == null) { return; } callbacks.remove(listener); } } private static void dispatchOnParseInitialized() { ParseCallbacks[] callbacks = collectParseCallbacks(); if (callbacks != null) { for (ParseCallbacks callback : callbacks) { callback.onParseInitialized(); } } } private static ParseCallbacks[] collectParseCallbacks() { ParseCallbacks[] callbacks; synchronized (MUTEX_CALLBACKS) { if (Parse.callbacks == null) { return null; } callbacks = new ParseCallbacks[Parse.callbacks.size()]; if (Parse.callbacks.size() > 0) { callbacks = Parse.callbacks.toArray(callbacks); } } return callbacks; } interface ParseCallbacks { void onParseInitialized(); } //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 Parse should do. */ public static void setLogLevel(int logLevel) { PLog.setLogLevel(logLevel); } /** * Returns the level of logging that will be displayed. */ public static int getLogLevel() { return PLog.getLogLevel(); } //endregion // Suppress constructor to prevent subclassing private Parse() { throw new AssertionError(); } static String externalVersionName() { return "a" + ParseObject.VERSION_NAME; } }