package com.mixpanel.android.mpmetrics; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import com.mixpanel.android.BuildConfig; import com.mixpanel.android.util.MPLog; import com.mixpanel.android.util.OfflineMode; import java.security.GeneralSecurityException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; /** * Stores global configuration options for the Mixpanel library. You can enable and disable configuration * options using <meta-data> tags inside of the <application> tag in your AndroidManifest.xml. * All settings are optional, and default to reasonable recommended values. Most users will not have to * set any options. * * Mixpanel understands the following options: * * <dl> * <dt>com.mixpanel.android.MPConfig.EnableDebugLogging</dt> * <dd>A boolean value. If true, emit more detailed log messages. Defaults to false</dd> * * <dt>com.mixpanel.android.MPConfig.BulkUploadLimit</dt> * <dd>An integer count of messages, the maximum number of messages to queue before an upload attempt. This value should be less than 50.</dd> * * <dt>com.mixpanel.android.MPConfig.FlushInterval</dt> * <dd>An integer number of milliseconds, the maximum time to wait before an upload if the bulk upload limit isn't reached.</dd> * * <dt>com.mixpanel.android.MPConfig.DebugFlushInterval</dt> * <dd>An integer number of milliseconds, the maximum time to wait before an upload if the bulk upload limit isn't reached in debug mode.</dd> * * <dt>com.mixpanel.android.MPConfig.DataExpiration</dt> * <dd>An integer number of milliseconds, the maximum age of records to send to Mixpanel. Corresponds to Mixpanel's server-side limit on record age.</dd> * * <dt>com.mixpanel.android.MPConfig.MinimumDatabaseLimit</dt> * <dd>An integer number of bytes. Mixpanel attempts to limit the size of its persistent data * queue based on the storage capacity of the device, but will always allow queing below this limit. Higher values * will take up more storage even when user storage is very full.</dd> * * <dt>com.mixpanel.android.MPConfig.DisableFallback</dt> * <dd>A boolean value. If true, do not send data over HTTP, even if HTTPS is unavailable. Defaults to true - by default, Mixpanel will only attempt to communicate over HTTPS.</dd> * * <dt>com.mixpanel.android.MPConfig.ResourcePackageName</dt> * <dd>A string java package name. Defaults to the package name of the Application. Users should set if the package name of their R class is different from the application package name due to application id settings.</dd> * * <dt>com.mixpanel.android.MPConfig.DisableGestureBindingUI</dt> * <dd>A boolean value. If true, do not allow connecting to the codeless event binding or A/B testing editor using an accelerometer gesture. Defaults to false.</dd> * * <dt>com.mixpanel.android.MPConfig.DisableEmulatorBindingUI</dt> * <dd>A boolean value. If true, do not attempt to connect to the codeless event binding or A/B testing editor when running in the Android emulator. Defaults to false.</dd> * * <dt>com.mixpanel.android.MPConfig.DisableAppOpenEvent</dt> * <dd>A boolean value. If true, do not send an "$app_open" event when the MixpanelAPI object is created for the first time. Defaults to true - the $app_open event will not be sent by default.</dd> * * <dt>com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates</dt> * <dd>A boolean value. If true, automatically show notifications and A/B test variants. Defaults to true.</dd> * * <dt>com.mixpanel.android.MPConfig.EventsEndpoint</dt> * <dd>A string URL. If present, the library will attempt to send events to this endpoint rather than to the default Mixpanel endpoint.</dd> * * <dt>com.mixpanel.android.MPConfig.EventsFallbackEndpoint</dt> * <dd>A string URL. If present, AND if DisableFallback is false, events will be sent to this endpoint if the EventsEndpoint cannot be reached.</dd> * * <dt>com.mixpanel.android.MPConfig.PeopleEndpoint</dt> * <dd>A string URL. If present, the library will attempt to send people updates to this endpoint rather than to the default Mixpanel endpoint.</dd> * * <dt>com.mixpanel.android.MPConfig.PeopleFallbackEndpoint</dt> * <dd>A string URL. If present, AND if DisableFallback is false, people updates will be sent to this endpoint if the EventsEndpoint cannot be reached.</dd> * * <dt>com.mixpanel.android.MPConfig.DecideEndpoint</dt> * <dd>A string URL. If present, the library will attempt to get notification, codeless event tracking, and A/B test variant information from this url rather than the default Mixpanel endpoint.</dd> * * <dt>com.mixpanel.android.MPConfig.DecideFallbackEndpoint</dt> * <dd>A string URL. If present, AND if DisableFallback is false, the library will query this url if the DecideEndpoint url cannot be reached.</dd> * * <dt>com.mixpanel.android.MPConfig.EditorUrl</dt> * <dd>A string URL. If present, the library will attempt to connect to this endpoint when in interactive editing mode, rather than to the default Mixpanel editor url.</dd> * * <dt>com.mixpanel.android.MPConfig.IgnoreInvisibleViewsVisualEditor</dt> * <dd>A boolean value. If true, invisible views won't be shown on Mixpanel Visual Editor (AB Test and codeless events) . Defaults to false.</dd> * </dl> * */ public class MPConfig { public static final String VERSION = BuildConfig.MIXPANEL_VERSION; public static boolean DEBUG = false; /** * Minimum API level for support of rich UI features, like In-App notifications and dynamic event binding. * Devices running OS versions below this level will still support tracking and push notification features. */ public static final int UI_FEATURES_MIN_API = 16; // Name for persistent storage of app referral SharedPreferences /* package */ static final String REFERRER_PREFS_NAME = "com.mixpanel.android.mpmetrics.ReferralInfo"; // Max size of the number of notifications we will hold in memory. Since they may contain images, // we don't want to suck up all of the memory on the device. /* package */ static final int MAX_NOTIFICATION_CACHE_COUNT = 2; // Instances are safe to store, since they're immutable and always the same. public static MPConfig getInstance(Context context) { synchronized (sInstanceLock) { if (null == sInstance) { final Context appContext = context.getApplicationContext(); sInstance = readConfig(appContext); } } return sInstance; } /** * The MixpanelAPI will use the system default SSL socket settings under ordinary circumstances. * That means it will ignore settings you associated with the default SSLSocketFactory in the * schema registry or in underlying HTTP libraries. If you'd prefer for Mixpanel to use your * own SSL settings, you'll need to call setSSLSocketFactory early in your code, like this * * {@code * <pre> * MPConfig.getInstance(context).setSSLSocketFactory(someCustomizedSocketFactory); * </pre> * } * * Your settings will be globally available to all Mixpanel instances, and will be used for * all SSL connections in the library. The call is thread safe, but should be done before * your first call to MixpanelAPI.getInstance to insure that the library never uses it's * default. * * The given socket factory may be used from multiple threads, which is safe for the system * SSLSocketFactory class, but if you pass a subclass you should ensure that it is thread-safe * before passing it to Mixpanel. * * @param factory an SSLSocketFactory that */ public synchronized void setSSLSocketFactory(SSLSocketFactory factory) { mSSLSocketFactory = factory; } /** * {@link OfflineMode} allows Mixpanel to be in-sync with client offline internal logic. * If you want to integrate your own logic with Mixpanel you'll need to call * {@link #setOfflineMode(OfflineMode)} early in your code, like this * * {@code * <pre> * MPConfig.getInstance(context).setOfflineMode(OfflineModeImplementation); * </pre> * } * * Your settings will be globally available to all Mixpanel instances, and will be used across * all the library. The call is thread safe, but should be done before * your first call to MixpanelAPI.getInstance to insure that the library never uses it's * default. * * The given {@link OfflineMode} may be used from multiple threads, you should ensure that * your implementation is thread-safe before passing it to Mixpanel. * * @param offlineMode client offline implementation to use on Mixpanel */ public synchronized void setOfflineMode(OfflineMode offlineMode) { mOfflineMode = offlineMode; } /* package */ MPConfig(Bundle metaData, Context context) { // By default, we use a clean, FACTORY default SSLSocket. In general this is the right // thing to do, and some other third party libraries change the SSLSocketFactory foundSSLFactory; try { final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, null); foundSSLFactory = sslContext.getSocketFactory(); } catch (final GeneralSecurityException e) { MPLog.i("MixpanelAPI.Conf", "System has no SSL support. Built-in events editor will not be available", e); foundSSLFactory = null; } mSSLSocketFactory = foundSSLFactory; DEBUG = metaData.getBoolean("com.mixpanel.android.MPConfig.EnableDebugLogging", false); if (metaData.containsKey("com.mixpanel.android.MPConfig.DebugFlushInterval")) { MPLog.w(LOGTAG, "We do not support com.mixpanel.android.MPConfig.DebugFlushInterval anymore. There will only be one flush interval. Please, update your AndroidManifest.xml."); } mBulkUploadLimit = metaData.getInt("com.mixpanel.android.MPConfig.BulkUploadLimit", 40); // 40 records default mFlushInterval = metaData.getInt("com.mixpanel.android.MPConfig.FlushInterval", 60 * 1000); // one minute default mDataExpiration = metaData.getInt("com.mixpanel.android.MPConfig.DataExpiration", 1000 * 60 * 60 * 24 * 5); // 5 days default mMinimumDatabaseLimit = metaData.getInt("com.mixpanel.android.MPConfig.MinimumDatabaseLimit", 20 * 1024 * 1024); // 20 Mb mDisableFallback = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableFallback", true); mResourcePackageName = metaData.getString("com.mixpanel.android.MPConfig.ResourcePackageName"); // default is null mDisableGestureBindingUI = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableGestureBindingUI", false); mDisableEmulatorBindingUI = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableEmulatorBindingUI", false); mDisableAppOpenEvent = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableAppOpenEvent", true); mDisableViewCrawler = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableViewCrawler", false); mDisableDecideChecker = metaData.getBoolean("com.mixpanel.android.MPConfig.DisableDecideChecker", false); mImageCacheMaxMemoryFactor = metaData.getInt("com.mixpanel.android.MPConfig.ImageCacheMaxMemoryFactor", 10); mIgnoreInvisibleViewsEditor = metaData.getBoolean("com.mixpanel.android.MPConfig.IgnoreInvisibleViewsVisualEditor", false); mAutoShowMixpanelUpdates = metaData.getBoolean("com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates", true); mNotificationDefaults = metaData.getInt("com.mixpanel.android.MPConfig.NotificationDefaults", 0); mTestMode = metaData.getBoolean("com.mixpanel.android.MPConfig.TestMode", false); String eventsEndpoint = metaData.getString("com.mixpanel.android.MPConfig.EventsEndpoint"); if (null == eventsEndpoint) { eventsEndpoint = "https://api.mixpanel.com/track?ip=1"; } mEventsEndpoint = eventsEndpoint; String eventsFallbackEndpoint = metaData.getString("com.mixpanel.android.MPConfig.EventsFallbackEndpoint"); if (null == eventsFallbackEndpoint) { eventsFallbackEndpoint = "http://api.mixpanel.com/track?ip=1"; } mEventsFallbackEndpoint = eventsFallbackEndpoint; String peopleEndpoint = metaData.getString("com.mixpanel.android.MPConfig.PeopleEndpoint"); if (null == peopleEndpoint) { peopleEndpoint = "https://api.mixpanel.com/engage"; } mPeopleEndpoint = peopleEndpoint; String peopleFallbackEndpoint = metaData.getString("com.mixpanel.android.MPConfig.PeopleFallbackEndpoint"); if (null == peopleFallbackEndpoint) { peopleFallbackEndpoint = "http://api.mixpanel.com/engage"; } mPeopleFallbackEndpoint = peopleFallbackEndpoint; String decideEndpoint = metaData.getString("com.mixpanel.android.MPConfig.DecideEndpoint"); if (null == decideEndpoint) { decideEndpoint = "https://decide.mixpanel.com/decide"; } mDecideEndpoint = decideEndpoint; String decideFallbackEndpoint = metaData.getString("com.mixpanel.android.MPConfig.DecideFallbackEndpoint"); if (null == decideFallbackEndpoint) { decideFallbackEndpoint = "http://decide.mixpanel.com/decide"; } mDecideFallbackEndpoint = decideFallbackEndpoint; String editorUrl = metaData.getString("com.mixpanel.android.MPConfig.EditorUrl"); if (null == editorUrl) { editorUrl = "wss://switchboard.mixpanel.com/connect/"; } mEditorUrl = editorUrl; int resourceId = metaData.getInt("com.mixpanel.android.MPConfig.DisableViewCrawlerForProjects", -1); if (resourceId != -1) { mDisableViewCrawlerForProjects = context.getResources().getStringArray(resourceId); } else { mDisableViewCrawlerForProjects = new String[0]; } MPLog.v(LOGTAG, "Mixpanel (" + VERSION + ") configured with:\n" + " AutoShowMixpanelUpdates " + getAutoShowMixpanelUpdates() + "\n" + " BulkUploadLimit " + getBulkUploadLimit() + "\n" + " FlushInterval " + getFlushInterval() + "\n" + " DataExpiration " + getDataExpiration() + "\n" + " MinimumDatabaseLimit " + getMinimumDatabaseLimit() + "\n" + " DisableFallback " + getDisableFallback() + "\n" + " DisableAppOpenEvent " + getDisableAppOpenEvent() + "\n" + " DisableViewCrawler " + getDisableViewCrawler() + "\n" + " DisableGestureBindingUI " + getDisableGestureBindingUI() + "\n" + " DisableEmulatorBindingUI " + getDisableEmulatorBindingUI() + "\n" + " EnableDebugLogging " + DEBUG + "\n" + " TestMode " + getTestMode() + "\n" + " EventsEndpoint " + getEventsEndpoint() + "\n" + " PeopleEndpoint " + getPeopleEndpoint() + "\n" + " DecideEndpoint " + getDecideEndpoint() + "\n" + " EventsFallbackEndpoint " + getEventsFallbackEndpoint() + "\n" + " PeopleFallbackEndpoint " + getPeopleFallbackEndpoint() + "\n" + " DecideFallbackEndpoint " + getDecideFallbackEndpoint() + "\n" + " EditorUrl " + getEditorUrl() + "\n" + " DisableDecideChecker " + getDisableDecideChecker() + "\n" + " IgnoreInvisibleViewsEditor " + getIgnoreInvisibleViewsEditor() + "\n" + " NotificationDefaults " + getNotificationDefaults() + "\n" ); } // Max size of queue before we require a flush. Must be below the limit the service will accept. public int getBulkUploadLimit() { return mBulkUploadLimit; } // Target max milliseconds between flushes. This is advisory. public int getFlushInterval() { return mFlushInterval; } // Throw away records that are older than this in milliseconds. Should be below the server side age limit for events. public int getDataExpiration() { return mDataExpiration; } public int getMinimumDatabaseLimit() { return mMinimumDatabaseLimit; } public boolean getDisableFallback() { return mDisableFallback; } public boolean getDisableGestureBindingUI() { return mDisableGestureBindingUI; } public boolean getDisableEmulatorBindingUI() { return mDisableEmulatorBindingUI; } public boolean getDisableAppOpenEvent() { return mDisableAppOpenEvent; } public boolean getDisableViewCrawler() { return mDisableViewCrawler; } public String[] getDisableViewCrawlerForProjects() { return mDisableViewCrawlerForProjects; } public boolean getTestMode() { return mTestMode; } // Preferred URL for tracking events public String getEventsEndpoint() { return mEventsEndpoint; } // Preferred URL for tracking people public String getPeopleEndpoint() { return mPeopleEndpoint; } // Preferred URL for pulling decide data public String getDecideEndpoint() { return mDecideEndpoint; } // Fallback URL for tracking events if post to preferred URL fails public String getEventsFallbackEndpoint() { return mEventsFallbackEndpoint; } // Fallback URL for tracking people if post to preferred URL fails public String getPeopleFallbackEndpoint() { return mPeopleFallbackEndpoint; } // Fallback URL for pulling decide data if preferred URL fails public String getDecideFallbackEndpoint() { return mDecideFallbackEndpoint; } // Check for and show eligible in app notifications on Activity changes public boolean getAutoShowMixpanelUpdates() { return mAutoShowMixpanelUpdates; } // Preferred URL for connecting to the editor websocket public String getEditorUrl() { return mEditorUrl; } public boolean getDisableDecideChecker() { return mDisableDecideChecker; } public boolean getIgnoreInvisibleViewsEditor() { return mIgnoreInvisibleViewsEditor; } public int getNotificationDefaults() { return mNotificationDefaults; } // Pre-configured package name for resources, if they differ from the application package name // // mContext.getPackageName() actually returns the "application id", which // usually (but not always) the same as package of the generated R class. // // See: http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename // // As far as I can tell, the original package name is lost in the build // process in these cases, and must be specified by the developer using // MPConfig meta-data. public String getResourcePackageName() { return mResourcePackageName; } // This method is thread safe, and assumes that SSLSocketFactory is also thread safe // (At this writing, all HttpsURLConnections in the framework share a single factory, // so this is pretty safe even if the docs are ambiguous) public synchronized SSLSocketFactory getSSLSocketFactory() { return mSSLSocketFactory; } // This method is thread safe, and assumes that OfflineMode is also thread safe public synchronized OfflineMode getOfflineMode() { return mOfflineMode; } // ImageStore LRU Cache size will be availableMaxMemory() / mImageCacheMaxMemoryFactor public int getImageCacheMaxMemoryFactor() { return mImageCacheMaxMemoryFactor; } /////////////////////////////////////////////// // Package access for testing only- do not call directly in library code /* package */ static MPConfig readConfig(Context appContext) { final String packageName = appContext.getPackageName(); try { final ApplicationInfo appInfo = appContext.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_META_DATA); Bundle configBundle = appInfo.metaData; if (null == configBundle) { configBundle = new Bundle(); } return new MPConfig(configBundle, appContext); } catch (final NameNotFoundException e) { throw new RuntimeException("Can't configure Mixpanel with package name " + packageName, e); } } private final int mBulkUploadLimit; private final int mFlushInterval; private final int mDataExpiration; private final int mMinimumDatabaseLimit; private final boolean mDisableFallback; private final boolean mTestMode; private final boolean mDisableGestureBindingUI; private final boolean mDisableEmulatorBindingUI; private final boolean mDisableAppOpenEvent; private final boolean mDisableViewCrawler; private final String[] mDisableViewCrawlerForProjects; private final String mEventsEndpoint; private final String mEventsFallbackEndpoint; private final String mPeopleEndpoint; private final String mPeopleFallbackEndpoint; private final String mDecideEndpoint; private final String mDecideFallbackEndpoint; private final boolean mAutoShowMixpanelUpdates; private final String mEditorUrl; private final String mResourcePackageName; private final boolean mDisableDecideChecker; private final int mImageCacheMaxMemoryFactor; private final boolean mIgnoreInvisibleViewsEditor; private final int mNotificationDefaults; // Mutable, with synchronized accessor and mutator private SSLSocketFactory mSSLSocketFactory; private OfflineMode mOfflineMode; private static MPConfig sInstance; private static final Object sInstanceLock = new Object(); private static final String LOGTAG = "MixpanelAPI.Conf"; }