package com.mixpanel.android.mpmetrics; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.app.FragmentTransaction; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import com.mixpanel.android.R; import com.mixpanel.android.takeoverinapp.TakeoverInAppActivity; import com.mixpanel.android.util.ActivityImageUtils; import com.mixpanel.android.util.MPLog; import com.mixpanel.android.viewcrawler.TrackingDebug; import com.mixpanel.android.viewcrawler.UpdatesFromMixpanel; import com.mixpanel.android.viewcrawler.ViewCrawler; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.ReentrantLock; /** * Core class for interacting with Mixpanel Analytics. * * <p>Call {@link #getInstance(Context, String)} with * your main application activity and your Mixpanel API token as arguments * an to get an instance you can use to report how users are using your * application. * * <p>Once you have an instance, you can send events to Mixpanel * using {@link #track(String, JSONObject)}, and update People Analytics * records with {@link #getPeople()} * * <p>The Mixpanel library will periodically send information to * Mixpanel servers, so your application will need to have * <tt>android.permission.INTERNET</tt>. In addition, to preserve * battery life, messages to Mixpanel servers may not be sent immediately * when you call <tt>track</tt> or {@link People#set(String, Object)}. * The library will send messages periodically throughout the lifetime * of your application, but you will need to call {@link #flush()} * before your application is completely shutdown to ensure all of your * events are sent. * * <p>A typical use-case for the library might look like this: * * <pre> * {@code * public class MainActivity extends Activity { * MixpanelAPI mMixpanel; * * public void onCreate(Bundle saved) { * mMixpanel = MixpanelAPI.getInstance(this, "YOUR MIXPANEL API TOKEN"); * ... * } * * public void whenSomethingInterestingHappens(int flavor) { * JSONObject properties = new JSONObject(); * properties.put("flavor", flavor); * mMixpanel.track("Something Interesting Happened", properties); * ... * } * * public void onDestroy() { * mMixpanel.flush(); * super.onDestroy(); * } * } * } * </pre> * * <p>In addition to this documentation, you may wish to take a look at * <a href="https://github.com/mixpanel/sample-android-mixpanel-integration">the Mixpanel sample Android application</a>. * It demonstrates a variety of techniques, including * updating People Analytics records with {@link People} and registering for * and receiving push notifications with {@link People#initPushHandling(String)}. * * <p>There are also <a href="https://mixpanel.com/docs/">step-by-step getting started documents</a> * available at mixpanel.com * * @see <a href="https://mixpanel.com/docs/integration-libraries/android">getting started documentation for tracking events</a> * @see <a href="https://mixpanel.com/docs/people-analytics/android">getting started documentation for People Analytics</a> * @see <a href="https://mixpanel.com/docs/people-analytics/android-push">getting started with push notifications for Android</a> * @see <a href="https://github.com/mixpanel/sample-android-mixpanel-integration">The Mixpanel Android sample application</a> */ public class MixpanelAPI { /** * String version of the library. */ public static final String VERSION = MPConfig.VERSION; /** * Declare a string-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<String> stringTweak(String tweakName, String defaultValue) { return sSharedTweaks.stringTweak(tweakName, defaultValue); } /** * Declare a boolean-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Boolean> booleanTweak(String tweakName, boolean defaultValue) { return sSharedTweaks.booleanTweak(tweakName, defaultValue); } /** * Declare a double-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Double> doubleTweak(String tweakName, double defaultValue) { return sSharedTweaks.doubleTweak(tweakName, defaultValue); } /** * Declare a float-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Float> floatTweak(String tweakName, float defaultValue) { return sSharedTweaks.floatTweak(tweakName, defaultValue); } /** * Declare a long-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Long> longTweak(String tweakName, long defaultValue) { return sSharedTweaks.longTweak(tweakName, defaultValue); } /** * Declare an int-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Integer> intTweak(String tweakName, int defaultValue) { return sSharedTweaks.intTweak(tweakName, defaultValue); } /** * Declare short-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Short> shortTweak(String tweakName, short defaultValue) { return sSharedTweaks.shortTweak(tweakName, defaultValue); } /** * Declare byte-valued tweak, and return a reference you can use to read the value of the tweak. * Tweaks can be changed in Mixpanel A/B tests, and can allow you to alter your customers' experience * in your app without re-deploying your application through the app store. */ public static Tweak<Byte> byteTweak(String tweakName, byte defaultValue) { return sSharedTweaks.byteTweak(tweakName, defaultValue); } /** * You shouldn't instantiate MixpanelAPI objects directly. * Use MixpanelAPI.getInstance to get an instance. */ MixpanelAPI(Context context, Future<SharedPreferences> referrerPreferences, String token) { this(context, referrerPreferences, token, MPConfig.getInstance(context)); } /** * You shouldn't instantiate MixpanelAPI objects directly. * Use MixpanelAPI.getInstance to get an instance. */ MixpanelAPI(Context context, Future<SharedPreferences> referrerPreferences, String token, MPConfig config) { mContext = context; mToken = token; mPeople = new PeopleImpl(); mConfig = config; final Map<String, String> deviceInfo = new HashMap<String, String>(); deviceInfo.put("$android_lib_version", MPConfig.VERSION); deviceInfo.put("$android_os", "Android"); deviceInfo.put("$android_os_version", Build.VERSION.RELEASE == null ? "UNKNOWN" : Build.VERSION.RELEASE); deviceInfo.put("$android_manufacturer", Build.MANUFACTURER == null ? "UNKNOWN" : Build.MANUFACTURER); deviceInfo.put("$android_brand", Build.BRAND == null ? "UNKNOWN" : Build.BRAND); deviceInfo.put("$android_model", Build.MODEL == null ? "UNKNOWN" : Build.MODEL); try { final PackageManager manager = mContext.getPackageManager(); final PackageInfo info = manager.getPackageInfo(mContext.getPackageName(), 0); deviceInfo.put("$android_app_version", info.versionName); deviceInfo.put("$android_app_version_code", Integer.toString(info.versionCode)); } catch (final PackageManager.NameNotFoundException e) { MPLog.e(LOGTAG, "Exception getting app version name", e); } mDeviceInfo = Collections.unmodifiableMap(deviceInfo); mUpdatesFromMixpanel = constructUpdatesFromMixpanel(context, token); mTrackingDebug = constructTrackingDebug(); mPersistentIdentity = getPersistentIdentity(context, referrerPreferences, token); mEventTimings = mPersistentIdentity.getTimeEvents(); mUpdatesListener = constructUpdatesListener(); mDecideMessages = constructDecideUpdates(token, mUpdatesListener, mUpdatesFromMixpanel); // TODO reading persistent identify immediately forces the lazy load of the preferences, and defeats the // purpose of PersistentIdentity's laziness. String decideId = mPersistentIdentity.getPeopleDistinctId(); if (null == decideId) { decideId = mPersistentIdentity.getEventsDistinctId(); } mDecideMessages.setDistinctId(decideId); mMessages = getAnalyticsMessages(); if (!mConfig.getDisableDecideChecker()) { mMessages.installDecideCheck(mDecideMessages); } registerMixpanelActivityLifecycleCallbacks(); if (sendAppOpen()) { track("$app_open", null); } if (!mPersistentIdentity.hasTrackedIntegration()) { try { final JSONObject messageProps = new JSONObject(); messageProps.put("mp_lib", "Android"); messageProps.put("lib", "Android"); messageProps.put("distinct_id", token); final AnalyticsMessages.EventDescription eventDescription = new AnalyticsMessages.EventDescription("Integration", messageProps, "85053bf24bba75239b16a601d9387e17"); mMessages.eventsMessage(eventDescription); flush(); mPersistentIdentity.setTrackedIntegration(true); } catch (JSONException e) { } } mUpdatesFromMixpanel.startUpdates(); } /** * Get the instance of MixpanelAPI associated with your Mixpanel project token. * * <p>Use getInstance to get a reference to a shared * instance of MixpanelAPI you can use to send events * and People Analytics updates to Mixpanel.</p> * <p>getInstance is thread safe, but the returned instance is not, * and may be shared with other callers of getInstance. * The best practice is to call getInstance, and use the returned MixpanelAPI, * object from a single thread (probably the main UI thread of your application).</p> * <p>If you do choose to track events from multiple threads in your application, * you should synchronize your calls on the instance itself, like so:</p> * <pre> * {@code * MixpanelAPI instance = MixpanelAPI.getInstance(context, token); * synchronized(instance) { // Only necessary if the instance will be used in multiple threads. * instance.track(...) * } * } * </pre> * * @param context The application context you are tracking * @param token Your Mixpanel project token. You can get your project token on the Mixpanel web site, * in the settings dialog. * @return an instance of MixpanelAPI associated with your project */ public static MixpanelAPI getInstance(Context context, String token) { if (null == token || null == context) { return null; } synchronized (sInstanceMap) { final Context appContext = context.getApplicationContext(); if (null == sReferrerPrefs) { sReferrerPrefs = sPrefsLoader.loadPreferences(context, MPConfig.REFERRER_PREFS_NAME, null); } Map <Context, MixpanelAPI> instances = sInstanceMap.get(token); if (null == instances) { instances = new HashMap<Context, MixpanelAPI>(); sInstanceMap.put(token, instances); } MixpanelAPI instance = instances.get(appContext); if (null == instance && ConfigurationChecker.checkBasicConfiguration(appContext)) { instance = new MixpanelAPI(appContext, sReferrerPrefs, token); registerAppLinksListeners(context, instance); instances.put(appContext, instance); } checkIntentForInboundAppLink(context); return instance; } } /** * This call is a no-op, and will be removed in future versions. * * @deprecated in 4.0.0, use com.mixpanel.android.MPConfig.FlushInterval application metadata instead */ @Deprecated public static void setFlushInterval(Context context, long milliseconds) { MPLog.i( LOGTAG, "MixpanelAPI.setFlushInterval is deprecated. Calling is now a no-op.\n" + " To set a custom Mixpanel flush interval for your application, add\n" + " <meta-data android:name=\"com.mixpanel.android.MPConfig.FlushInterval\" android:value=\"YOUR_INTERVAL\" />\n" + " to the <application> section of your AndroidManifest.xml." ); } /** * This call is a no-op, and will be removed in future versions of the library. * * @deprecated in 4.0.0, use com.mixpanel.android.MPConfig.EventsFallbackEndpoint, com.mixpanel.android.MPConfig.PeopleFallbackEndpoint, or com.mixpanel.android.MPConfig.DecideFallbackEndpoint instead */ @Deprecated public static void enableFallbackServer(Context context, boolean enableIfTrue) { MPLog.i( LOGTAG, "MixpanelAPI.enableFallbackServer is deprecated. This call is a no-op.\n" + " To enable fallback in your application, add\n" + " <meta-data android:name=\"com.mixpanel.android.MPConfig.DisableFallback\" android:value=\"false\" />\n" + " to the <application> section of your AndroidManifest.xml." ); } /** * This function creates a distinct_id alias from alias to original. If original is null, then it will create an alias * to the current events distinct_id, which may be the distinct_id randomly generated by the Mixpanel library * before {@link #identify(String)} is called. * * <p>This call does not identify the user after. You must still call both {@link #identify(String)} and * {@link People#identify(String)} if you wish the new alias to be used for Events and People. * * @param alias the new distinct_id that should represent original. * @param original the old distinct_id that alias will be mapped to. */ public void alias(String alias, String original) { if (original == null) { original = getDistinctId(); } if (alias.equals(original)) { MPLog.w(LOGTAG, "Attempted to alias identical distinct_ids " + alias + ". Alias message will not be sent."); return; } try { final JSONObject j = new JSONObject(); j.put("alias", alias); j.put("original", original); track("$create_alias", j); } catch (final JSONException e) { MPLog.e(LOGTAG, "Failed to alias", e); } flush(); } /** * Associate all future calls to {@link #track(String, JSONObject)} with the user identified by * the given distinct id. * * <p>This call does not identify the user for People Analytics; * to do that, see {@link People#identify(String)}. Mixpanel recommends using * the same distinct_id for both calls, and using a distinct_id that is easy * to associate with the given user, for example, a server-side account identifier. * * <p>Calls to {@link #track(String, JSONObject)} made before corresponding calls to * identify will use an internally generated distinct id, which means it is best * to call identify early to ensure that your Mixpanel funnels and retention * analytics can continue to track the user throughout their lifetime. We recommend * calling identify as early as you can. * * <p>Once identify is called, the given distinct id persists across restarts of your * application. * * @param distinctId a string uniquely identifying this user. Events sent to * Mixpanel using the same disinct_id will be considered associated with the * same visitor/customer for retention and funnel reporting, so be sure that the given * value is globally unique for each individual user you intend to track. * * @see People#identify(String) */ public void identify(String distinctId) { synchronized (mPersistentIdentity) { mPersistentIdentity.setEventsDistinctId(distinctId); String decideId = mPersistentIdentity.getPeopleDistinctId(); if (null == decideId) { decideId = mPersistentIdentity.getEventsDistinctId(); } mDecideMessages.setDistinctId(decideId); } } /** * Begin timing of an event. Calling timeEvent("Thing") will not send an event, but * when you eventually call track("Thing"), your tracked event will be sent with a "$duration" * property, representing the number of seconds between your calls. * * @param eventName the name of the event to track with timing. */ public void timeEvent(final String eventName) { final long writeTime = System.currentTimeMillis(); synchronized (mEventTimings) { mEventTimings.put(eventName, writeTime); mPersistentIdentity.addTimeEvent(eventName, writeTime); } } /** * Track an event. * * <p>Every call to track eventually results in a data point sent to Mixpanel. These data points * are what are measured, counted, and broken down to create your Mixpanel reports. Events * have a string name, and an optional set of name/value pairs that describe the properties of * that event. * * @param eventName The name of the event to send * @param properties A Map containing the key value pairs of the properties to include in this event. * Pass null if no extra properties exist. * * See also {@link #track(String, org.json.JSONObject)} */ public void trackMap(String eventName, Map<String, Object> properties) { if (null == properties) { track(eventName, null); } else { try { track(eventName, new JSONObject(properties)); } catch (NullPointerException e) { MPLog.w(LOGTAG, "Can't have null keys in the properties of trackMap!"); } } } /** * Track an event. * * <p>Every call to track eventually results in a data point sent to Mixpanel. These data points * are what are measured, counted, and broken down to create your Mixpanel reports. Events * have a string name, and an optional set of name/value pairs that describe the properties of * that event. * * @param eventName The name of the event to send * @param properties A JSONObject containing the key value pairs of the properties to include in this event. * Pass null if no extra properties exist. */ // DO NOT DOCUMENT, but track() must be thread safe since it is used to track events in // notifications from the UI thread, which might not be our MixpanelAPI "home" thread. // This MAY CHANGE IN FUTURE RELEASES, so minimize code that assumes thread safety // (and perhaps document that code here). public void track(String eventName, JSONObject properties) { final Long eventBegin; synchronized (mEventTimings) { eventBegin = mEventTimings.get(eventName); mEventTimings.remove(eventName); mPersistentIdentity.removeTimeEvent(eventName); } try { final JSONObject messageProps = new JSONObject(); final Map<String, String> referrerProperties = mPersistentIdentity.getReferrerProperties(); for (final Map.Entry<String, String> entry : referrerProperties.entrySet()) { final String key = entry.getKey(); final String value = entry.getValue(); messageProps.put(key, value); } mPersistentIdentity.addSuperPropertiesToObject(messageProps); // Don't allow super properties or referral properties to override these fields, // but DO allow the caller to override them in their given properties. final double timeSecondsDouble = (System.currentTimeMillis()) / 1000.0; final long timeSeconds = (long) timeSecondsDouble; messageProps.put("time", timeSeconds); messageProps.put("distinct_id", getDistinctId()); if (null != eventBegin) { final double eventBeginDouble = ((double) eventBegin) / 1000.0; final double secondsElapsed = timeSecondsDouble - eventBeginDouble; messageProps.put("$duration", secondsElapsed); } if (null != properties) { final Iterator<?> propIter = properties.keys(); while (propIter.hasNext()) { final String key = (String) propIter.next(); messageProps.put(key, properties.get(key)); } } final AnalyticsMessages.EventDescription eventDescription = new AnalyticsMessages.EventDescription(eventName, messageProps, mToken); mMessages.eventsMessage(eventDescription); if (null != mTrackingDebug) { mTrackingDebug.reportTrack(eventName); } } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception tracking event " + eventName, e); } } /** * Equivalent to {@link #track(String, JSONObject)} with a null argument for properties. * Consider adding properties to your tracking to get the best insights and experience from Mixpanel. * @param eventName the name of the event to send */ public void track(String eventName) { track(eventName, null); } /** * Push all queued Mixpanel events and People Analytics changes to Mixpanel servers. * * <p>Events and People messages are pushed gradually throughout * the lifetime of your application. This means that to ensure that all messages * are sent to Mixpanel when your application is shut down, you will * need to call flush() to let the Mixpanel library know it should * send all remaining messages to the server. We strongly recommend * placing a call to flush() in the onDestroy() method of * your main application activity. */ public void flush() { mMessages.postToServer(); } /** * Returns a json object of the user's current super properties * *<p>SuperProperties are a collection of properties that will be sent with every event to Mixpanel, * and persist beyond the lifetime of your application. */ public JSONObject getSuperProperties() { JSONObject ret = new JSONObject(); mPersistentIdentity.addSuperPropertiesToObject(ret); return ret; } /** * Returns the string id currently being used to uniquely identify the user associated * with events sent using {@link #track(String, JSONObject)}. Before any calls to * {@link #identify(String)}, this will be an id automatically generated by the library. * * <p>The id returned by getDistinctId is independent of the distinct id used to identify * any People Analytics properties in Mixpanel. To read and write that identifier, * use {@link People#identify(String)} and {@link People#getDistinctId()}. * * @return The distinct id associated with event tracking * * @see #identify(String) * @see People#getDistinctId() */ public String getDistinctId() { return mPersistentIdentity.getEventsDistinctId(); } /** * Register properties that will be sent with every subsequent call to {@link #track(String, JSONObject)}. * * <p>SuperProperties are a collection of properties that will be sent with every event to Mixpanel, * and persist beyond the lifetime of your application. * * <p>Setting a superProperty with registerSuperProperties will store a new superProperty, * possibly overwriting any existing superProperty with the same name (to set a * superProperty only if it is currently unset, use {@link #registerSuperPropertiesOnce(JSONObject)}) * * <p>SuperProperties will persist even if your application is taken completely out of memory. * to remove a superProperty, call {@link #unregisterSuperProperty(String)} or {@link #clearSuperProperties()} * * @param superProperties A Map containing super properties to register * * See also {@link #registerSuperProperties(org.json.JSONObject)} */ public void registerSuperPropertiesMap(Map<String, Object> superProperties) { if (null == superProperties) { MPLog.e(LOGTAG, "registerSuperPropertiesMap does not accept null properties"); return; } try { registerSuperProperties(new JSONObject(superProperties)); } catch (NullPointerException e) { MPLog.w(LOGTAG, "Can't have null keys in the properties of registerSuperPropertiesMap"); } } /** * Register properties that will be sent with every subsequent call to {@link #track(String, JSONObject)}. * * <p>SuperProperties are a collection of properties that will be sent with every event to Mixpanel, * and persist beyond the lifetime of your application. * * <p>Setting a superProperty with registerSuperProperties will store a new superProperty, * possibly overwriting any existing superProperty with the same name (to set a * superProperty only if it is currently unset, use {@link #registerSuperPropertiesOnce(JSONObject)}) * * <p>SuperProperties will persist even if your application is taken completely out of memory. * to remove a superProperty, call {@link #unregisterSuperProperty(String)} or {@link #clearSuperProperties()} * * @param superProperties A JSONObject containing super properties to register * @see #registerSuperPropertiesOnce(JSONObject) * @see #unregisterSuperProperty(String) * @see #clearSuperProperties() */ public void registerSuperProperties(JSONObject superProperties) { mPersistentIdentity.registerSuperProperties(superProperties); } /** * Remove a single superProperty, so that it will not be sent with future calls to {@link #track(String, JSONObject)}. * * <p>If there is a superProperty registered with the given name, it will be permanently * removed from the existing superProperties. * To clear all superProperties, use {@link #clearSuperProperties()} * * @param superPropertyName name of the property to unregister * @see #registerSuperProperties(JSONObject) */ public void unregisterSuperProperty(String superPropertyName) { mPersistentIdentity.unregisterSuperProperty(superPropertyName); } /** * Register super properties for events, only if no other super property with the * same names has already been registered. * * <p>Calling registerSuperPropertiesOnce will never overwrite existing properties. * * @param superProperties A Map containing the super properties to register. * * See also {@link #registerSuperPropertiesOnce(org.json.JSONObject)} */ public void registerSuperPropertiesOnceMap(Map<String, Object> superProperties) { if (null == superProperties) { MPLog.e(LOGTAG, "registerSuperPropertiesOnceMap does not accept null properties"); return; } try { registerSuperPropertiesOnce(new JSONObject(superProperties)); } catch (NullPointerException e) { MPLog.w(LOGTAG, "Can't have null keys in the properties of registerSuperPropertiesOnce!"); } } /** * Register super properties for events, only if no other super property with the * same names has already been registered. * * <p>Calling registerSuperPropertiesOnce will never overwrite existing properties. * * @param superProperties A JSONObject containing the super properties to register. * @see #registerSuperProperties(JSONObject) */ public void registerSuperPropertiesOnce(JSONObject superProperties) { mPersistentIdentity.registerSuperPropertiesOnce(superProperties); } /** * Erase all currently registered superProperties. * * <p>Future tracking calls to Mixpanel will not contain the specific * superProperties registered before the clearSuperProperties method was called. * * <p>To remove a single superProperty, use {@link #unregisterSuperProperty(String)} * * @see #registerSuperProperties(JSONObject) */ public void clearSuperProperties() { mPersistentIdentity.clearSuperProperties(); } /** * Updates super properties in place. Given a SuperPropertyUpdate object, will * pass the current values of SuperProperties to that update and replace all * results with the return value of the update. Updates are synchronized on * the underlying super properties store, so they are guaranteed to be thread safe * (but long running updates may slow down your tracking.) * * @param update A function from one set of super properties to another. The update should not return null. */ public void updateSuperProperties(SuperPropertyUpdate update) { mPersistentIdentity.updateSuperProperties(update); } /** * Returns a Mixpanel.People object that can be used to set and increment * People Analytics properties. * * @return an instance of {@link People} that you can use to update * records in Mixpanel People Analytics and manage Mixpanel Google Cloud Messaging notifications. */ public People getPeople() { return mPeople; } /** * Clears all distinct_ids, superProperties, and push registrations from persistent storage. * Will not clear referrer information. */ public void reset() { // Will clear distinct_ids, superProperties, notifications, experiments, // and waiting People Analytics properties. Will have no effect // on messages already queued to send with AnalyticsMessages. mPersistentIdentity.clearPreferences(); identify(getDistinctId()); flush(); } /** * Returns an unmodifiable map that contains the device description properties * that will be sent to Mixpanel. These are not all of the default properties, * but are a subset that are dependant on the user's device or installed version * of the host application, and are guaranteed not to change while the app is running. */ public Map<String, String> getDeviceInfo() { return mDeviceInfo; } /** * Core interface for using Mixpanel People Analytics features. * You can get an instance by calling {@link MixpanelAPI#getPeople()} * * <p>The People object is used to update properties in a user's People Analytics record, * and to manage the receipt of push notifications sent via Mixpanel Engage. * For this reason, it's important to call {@link #identify(String)} on the People * object before you work with it. Once you call identify, the user identity will * persist across stops and starts of your application, until you make another * call to identify using a different id. * * A typical use case for the People object might look like this: * * <pre> * {@code * * public class MainActivity extends Activity { * MixpanelAPI mMixpanel; * * public void onCreate(Bundle saved) { * mMixpanel = MixpanelAPI.getInstance(this, "YOUR MIXPANEL API TOKEN"); * mMixpanel.getPeople().identify("A UNIQUE ID FOR THIS USER"); * mMixpanel.getPeople().initPushHandling("YOUR 12 DIGIT GOOGLE SENDER API"); * ... * } * * public void userUpdatedJobTitle(String newTitle) { * mMixpanel.getPeople().set("Job Title", newTitle); * ... * } * * public void onDestroy() { * mMixpanel.flush(); * super.onDestroy(); * } * } * * } * </pre> * * @see MixpanelAPI */ public interface People { /** * Associate future calls to {@link #set(JSONObject)}, {@link #increment(Map)}, * and {@link #initPushHandling(String)} with a particular People Analytics user. * * <p>All future calls to the People object will rely on this value to assign * and increment properties. The user identification will persist across * restarts of your application. We recommend calling * People.identify as soon as you know the distinct id of the user. * * @param distinctId a String that uniquely identifies the user. Users identified with * the same distinct id will be considered to be the same user in Mixpanel, * across all platforms and devices. We recommend choosing a distinct id * that is meaningful to your other systems (for example, a server-side account * identifier), and using the same distinct id for both calls to People.identify * and {@link MixpanelAPI#identify(String)} * * @see MixpanelAPI#identify(String) */ public void identify(String distinctId); /** * Sets a single property with the given name and value for this user. * The given name and value will be assigned to the user in Mixpanel People Analytics, * possibly overwriting an existing property with the same name. * * @param propertyName The name of the Mixpanel property. This must be a String, for example "Zip Code" * @param value The value of the Mixpanel property. For "Zip Code", this value might be the String "90210" */ public void set(String propertyName, Object value); /** * Set a collection of properties on the identified user all at once. * * @param properties a Map containing the collection of properties you wish to apply * to the identified user. Each key in the Map will be associated with * a property name, and the value of that key will be assigned to the property. * * See also {@link #set(org.json.JSONObject)} */ public void setMap(Map<String, Object> properties); /** * Set a collection of properties on the identified user all at once. * * @param properties a JSONObject containing the collection of properties you wish to apply * to the identified user. Each key in the JSONObject will be associated with * a property name, and the value of that key will be assigned to the property. */ public void set(JSONObject properties); /** * Works just like {@link People#set(String, Object)}, except it will not overwrite existing property values. This is useful for properties like "First login date". * * @param propertyName The name of the Mixpanel property. This must be a String, for example "Zip Code" * @param value The value of the Mixpanel property. For "Zip Code", this value might be the String "90210" */ public void setOnce(String propertyName, Object value); /** * Like {@link People#set(String, Object)}, but will not set properties that already exist on a record. * * @param properties a Map containing the collection of properties you wish to apply * to the identified user. Each key in the Map will be associated with * a property name, and the value of that key will be assigned to the property. * * See also {@link #setOnce(org.json.JSONObject)} */ public void setOnceMap(Map<String, Object> properties); /** * Like {@link People#set(String, Object)}, but will not set properties that already exist on a record. * * @param properties a JSONObject containing the collection of properties you wish to apply * to the identified user. Each key in the JSONObject will be associated with * a property name, and the value of that key will be assigned to the property. */ public void setOnce(JSONObject properties); /** * Add the given amount to an existing property on the identified user. If the user does not already * have the associated property, the amount will be added to zero. To reduce a property, * provide a negative number for the value. * * @param name the People Analytics property that should have its value changed * @param increment the amount to be added to the current value of the named property * * @see #increment(Map) */ public void increment(String name, double increment); /** * Merge a given JSONObject into the object-valued property named name. If the user does not * already have the associated property, an new property will be created with the value of * the given updates. If the user already has a value for the given property, the updates will * be merged into the existing value, with key/value pairs in updates taking precedence over * existing key/value pairs where the keys are the same. * * @param name the People Analytics property that should have the update merged into it * @param updates a JSONObject with keys and values that will be merged into the property */ public void merge(String name, JSONObject updates); /** * Change the existing values of multiple People Analytics properties at once. * * <p>If the user does not already have the associated property, the amount will * be added to zero. To reduce a property, provide a negative number for the value. * * @param properties A map of String properties names to Long amounts. Each * property associated with a name in the map will have its value changed by the given amount * * @see #increment(String, double) */ public void increment(Map<String, ? extends Number> properties); /** * Appends a value to a list-valued property. If the property does not currently exist, * it will be created as a list of one element. If the property does exist and doesn't * currently have a list value, the append will be ignored. * @param name the People Analytics property that should have it's value appended to * @param value the new value that will appear at the end of the property's list */ public void append(String name, Object value); /** * Adds values to a list-valued property only if they are not already present in the list. * If the property does not currently exist, it will be created with the given list as it's value. * If the property exists and is not list-valued, the union will be ignored. * * @param name name of the list-valued property to set or modify * @param value an array of values to add to the property value if not already present */ void union(String name, JSONArray value); /** * Remove value from a list-valued property only if they are already present in the list. * If the property does not currently exist, the remove will be ignored. * If the property exists and is not list-valued, the remove will be ignored. * @param name the People Analytics property that should have it's value removed from * @param value the value that will be removed from the property's list */ public void remove(String name, Object value); /** * permanently removes the property with the given name from the user's profile * @param name name of a property to unset */ void unset(String name); /** * Track a revenue transaction for the identified people profile. * * @param amount the amount of money exchanged. Positive amounts represent purchases or income from the customer, negative amounts represent refunds or payments to the customer. * @param properties an optional collection of properties to associate with this transaction. */ public void trackCharge(double amount, JSONObject properties); /** * Permanently clear the whole transaction history for the identified people profile. */ public void clearCharges(); /** * Permanently deletes the identified user's record from People Analytics. * * <p>Calling deleteUser deletes an entire record completely. Any future calls * to People Analytics using the same distinct id will create and store new values. */ public void deleteUser(); /** * Enable end-to-end Google Cloud Messaging (GCM) from Mixpanel. * * <p>Calling this method will allow the Mixpanel libraries to handle GCM user * registration, and enable Mixpanel to show alerts when GCM messages arrive. * * <p>To use {@link People#initPushHandling}, you will need to add the following to your application manifest: * * <pre> * {@code * <receiver android:name="com.mixpanel.android.mpmetrics.GCMReceiver" * android:permission="com.google.android.c2dm.permission.SEND" > * <intent-filter> * <action android:name="com.google.android.c2dm.intent.RECEIVE" /> * <action android:name="com.google.android.c2dm.intent.REGISTRATION" /> * <category android:name="YOUR_PACKAGE_NAME" /> * </intent-filter> * </receiver> * } * </pre> * * Be sure to replace "YOUR_PACKAGE_NAME" with the name of your package. For * more information and a list of necessary permissions, see {@link GCMReceiver}. * * <p>If you're planning to use end-to-end support for Messaging, we recommend you * call this method immediately after calling {@link People#identify(String)}, likely * early in your application's life cycle. (for example, in the onCreate method of your * main application activity.) * * <p>Calls to {@link People#initPushHandling} should not be mixed with calls to * {@link #setPushRegistrationId(String)} and {@link #clearPushRegistrationId()} * in the same application. Application authors should choose one or the other * method for handling Mixpanel GCM messages. * * @param senderID of the Google API Project that registered for Google Cloud Messaging * You can find your ID by looking at the URL of in your Google API Console * at https://code.google.com/apis/console/; it is the twelve digit number after * after "#project:" in the URL address bar on console pages. * * @see com.mixpanel.android.mpmetrics.GCMReceiver * @see <a href="https://mixpanel.com/docs/people-analytics/android-push">Getting Started with Android Push Notifications</a> */ public void initPushHandling(String senderID); /** * Retrieves Google Cloud Messaging registration ID. * * <p>{@link People#getPushRegistrationId} should only be called after {@link #identify(String)} has been called. * * @return GCM push token or null if the user has not been registered in GCM. * * @see #initPushHandling(String) * @see #setPushRegistrationId(String) */ public String getPushRegistrationId(); /** * Manually send a Google Cloud Messaging registration id to Mixpanel. * * <p>If you are handling Google Cloud Messages in your own application, but would like to * allow Mixpanel to handle messages originating from Mixpanel campaigns, you should * call setPushRegistrationId with the "registration_id" property of the * com.google.android.c2dm.intent.REGISTRATION intent when it is received. * * <p>setPushRegistrationId should only be called after {@link #identify(String)} has been called. * * <p>Calls to {@link People#setPushRegistrationId} should not be mixed with calls to {@link #initPushHandling(String)} * in the same application. In addition, applications that call setPushRegistrationId * should also call {@link #clearPushRegistrationId()} when they receive an intent to unregister * (a com.google.android.c2dm.intent.REGISTRATION intent with getStringExtra("unregistered") != null) * * @param registrationId the result of calling intent.getStringExtra("registration_id") * on a com.google.android.c2dm.intent.REGISTRATION intent * * @see #initPushHandling(String) * @see #clearPushRegistrationId() */ public void setPushRegistrationId(String registrationId); /** * Manually clear all current Google Cloud Messaging registration ids from Mixpanel. * * <p>If you are handling Google Cloud Messages in your own application, you should * call this method when your application receives a com.google.android.c2dm.intent.REGISTRATION * with getStringExtra("unregistered") != null * * <p>{@link People#clearPushRegistrationId} should only be called after {@link #identify(String)} has been called. * * <p>In general, all applications that call {@link #setPushRegistrationId(String)} should include a call to * clearPushRegistrationId, and no applications that call * {@link #initPushHandling(String)} should call clearPushRegistrationId */ public void clearPushRegistrationId(); /** * Manually clear a single Google Cloud Messaging registration id from Mixpanel. * * <p>If you are handling Google Cloud Messages in your own application, you should * call this method when your application receives a com.google.android.c2dm.intent.REGISTRATION * with getStringExtra("unregistered") != null * * <p>{@link People#clearPushRegistrationId} should only be called after {@link #identify(String)} has been called. * * <p>In general, all applications that call {@link #setPushRegistrationId(String)} should include a call to * clearPushRegistrationId, and no applications that call * {@link #initPushHandling(String)} should call clearPushRegistrationId */ public void clearPushRegistrationId(String registrationId); /** * Returns the string id currently being used to uniquely identify the user associated * with events sent using {@link People#set(String, Object)} and {@link People#increment(String, double)}. * If no calls to {@link People#identify(String)} have been made, this method will return null. * * <p>The id returned by getDistinctId is independent of the distinct id used to identify * any events sent with {@link MixpanelAPI#track(String, JSONObject)}. To read and write that identifier, * use {@link MixpanelAPI#identify(String)} and {@link MixpanelAPI#getDistinctId()}. * * @return The distinct id associated with updates to People Analytics * * @see People#identify(String) * @see MixpanelAPI#getDistinctId() */ public String getDistinctId(); /** * Shows an in-app notification to the user if one is available. If the notification * is a mini notification, this method will attach and remove a Fragment to parent. * The lifecycle of the Fragment will be handled entirely by the Mixpanel library. * * <p>If the notification is a takeover notification, a TakeoverInAppActivity will be launched to * display the Takeover notification. * * <p>It is safe to call this method any time you want to potentially display an in-app notification. * This method will be a no-op if there is already an in-app notification being displayed. * * <p>This method is a no-op in environments with * Android API before JellyBean/API level 16. * * @param parent the Activity that the mini notification will be displayed in, or the Activity * that will be used to launch TakeoverInAppActivity for the takeover notification. */ public void showNotificationIfAvailable(Activity parent); /** * Applies A/B test changes, if they are present. By default, your application will attempt * to join available experiments any time an activity is resumed, but you can disable this * automatic behavior by adding the following tag to the <application> tag in your AndroidManifest.xml * {@code * <meta-data android:name="com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates" * android:value="false" /> * } * * If you disable AutoShowMixpanelUpdates, you'll need to call joinExperimentIfAvailable to * join or clear existing experiments. If you want to display a loading screen or otherwise * wait for experiments to load from the server before you apply them, you can use * {@link #addOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener)} to * be informed that new experiments are ready. */ public void joinExperimentIfAvailable(); /** * Shows the given in-app notification to the user. Display will occur just as if the * notification was shown via showNotificationIfAvailable. In most cases, it is * easier and more efficient to use showNotificationIfAvailable. * * @param notif the {@link com.mixpanel.android.mpmetrics.InAppNotification} to show * * @param parent the Activity that the mini notification will be displayed in, or the Activity * that will be used to launch TakeoverInAppActivity for the takeover notification. */ public void showGivenNotification(InAppNotification notif, Activity parent); /** * Sends an event to Mixpanel that includes the automatic properties associated * with the given notification. In most cases this is not required, unless you're * not showing notifications using the library-provided in views and activities. * * @param eventName the name to use when the event is tracked. * * @param notif the {@link com.mixpanel.android.mpmetrics.InAppNotification} associated with the event you'd like to track. */ public void trackNotification(String eventName, InAppNotification notif); /** * Returns an InAppNotification object if one is available and being held by the library, or null if * no notification is currently available. Callers who want to display in-app notifications should call this * method periodically. A given InAppNotification will be returned only once from this method, so callers * should be ready to consume any non-null return value. * * <p>This function will return quickly, and will not cause any communication with * Mixpanel's servers, so it is safe to call this from the UI thread. * * Note: you must call call {@link People#trackNotificationSeen(InAppNotification)} or you will * receive the same {@link com.mixpanel.android.mpmetrics.InAppNotification} again the * next time notifications are refreshed from Mixpanel's servers (on identify, or when * your app is destroyed and re-created) * * @return an InAppNotification object if one is available, null otherwise. */ public InAppNotification getNotificationIfAvailable(); /** * Tells MixPanel that you have handled an {@link com.mixpanel.android.mpmetrics.InAppNotification} * in the case where you are manually dealing with your notifications ({@link People#getNotificationIfAvailable()}). * * Note: if you do not acknowledge the notification you will receive it again each time * you call {@link People#identify(String)} and then call {@link People#getNotificationIfAvailable()} * * @param notif the notification to track (no-op on null) */ void trackNotificationSeen(InAppNotification notif); /** * Shows an in-app notification identified by id. The behavior of this is otherwise identical to * {@link People#showNotificationIfAvailable(Activity)}. * * @param id the id of the InAppNotification you wish to show. * @param parent the Activity that the mini notification will be displayed in, or the Activity * that will be used to launch TakeoverInAppActivity for the takeover notification. */ public void showNotificationById(int id, final Activity parent); /** * Return an instance of Mixpanel people with a temporary distinct id. * Instances returned by withIdentity will not check decide with the given distinctId. */ public People withIdentity(String distinctId); /** * Adds a new listener that will receive a callback when new updates from Mixpanel * (like in-app notifications or A/B test experiments) are discovered. Most users of the library * will not need this method since in-app notifications and experiments are * applied automatically to your application by default. * * <p>The given listener will be called when a new batch of updates is detected. Handlers * should be prepared to handle the callback on an arbitrary thread. * * <p>The listener will be called when new in-app notifications or experiments * are detected as available. That means you wait to call * {@link People#showNotificationIfAvailable(Activity)}, and {@link People#joinExperimentIfAvailable()} * to show content and updates that have been delivered to your app. (You can also call these * functions whenever else you would like, they're inexpensive and will do nothing if no * content is available.) * * @param listener the listener to add */ public void addOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener); /** * Removes a listener previously registered with addOnMixpanelUpdatesReceivedListener. * * @param listener the listener to add */ public void removeOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener); /** * Sets the listener that will receive a callback when new Tweaks from Mixpanel are discovered. Most * users of the library will not need this method, since Tweaks are applied automatically to your * application by default. * * <p>The given listener will be called when a new batch of Tweaks is applied. Handlers * should be prepared to handle the callback on an arbitrary thread. * * <p>The listener will be called when new Tweaks are detected as available. That means the listener * will get called once {@link People#joinExperimentIfAvailable()} has successfully applied the changes. * * @param listener the listener to set */ public void addOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener); /** * Removes the listener previously registered with addOnMixpanelTweaksUpdatedListener. * */ public void removeOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener); } /** * This method is a no-op, kept for compatibility purposes. * * To enable verbose logging about communication with Mixpanel, add * {@code * <meta-data android:name="com.mixpanel.android.MPConfig.EnableDebugLogging" /> * } * * To the {@code <application>} tag of your AndroidManifest.xml file. * * @deprecated in 4.1.0, use Manifest meta-data instead */ @Deprecated public void logPosts() { MPLog.i( LOGTAG, "MixpanelAPI.logPosts() is deprecated.\n" + " To get verbose debug level logging, add\n" + " <meta-data android:name=\"com.mixpanel.android.MPConfig.EnableDebugLogging\" value=\"true\" />\n" + " to the <application> section of your AndroidManifest.xml." ); } /** * Attempt to register MixpanelActivityLifecycleCallbacks to the application's event lifecycle. * Once registered, we can automatically check for and show in-app notifications * when any Activity is opened. * * This is only available if the android version is >= 16. You can disable livecycle callbacks by setting * com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates to false in your AndroidManifest.xml * * This function is automatically called when the library is initialized unless you explicitly * set com.mixpanel.android.MPConfig.AutoShowMixpanelUpdates to false in your AndroidManifest.xml */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) /* package */ void registerMixpanelActivityLifecycleCallbacks() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (mContext.getApplicationContext() instanceof Application) { final Application app = (Application) mContext.getApplicationContext(); mMixpanelActivityLifecycleCallbacks = new MixpanelActivityLifecycleCallbacks(this, mConfig); app.registerActivityLifecycleCallbacks(mMixpanelActivityLifecycleCallbacks); } else { MPLog.i(LOGTAG, "Context is not an Application, Mixpanel will not automatically show in-app notifications or A/B test experiments. We won't be able to automatically flush on an app background."); } } } /** * Based on the application's event lifecycle this method will determine whether the app * is running in the foreground or not. * * If your build version is below 14 this method will always return false. * * @return True if the app is running in the foreground. */ public boolean isAppInForeground() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { if (mMixpanelActivityLifecycleCallbacks != null) { return mMixpanelActivityLifecycleCallbacks.isInForeground(); } } else { MPLog.e(LOGTAG, "Your build version is below 14. This method will always return false."); } return false; } // Package-level access. Used (at least) by GCMReceiver // when OS-level events occur. /* package */ interface InstanceProcessor { public void process(MixpanelAPI m); } /* package */ static void allInstances(InstanceProcessor processor) { synchronized (sInstanceMap) { for (final Map<Context, MixpanelAPI> contextInstances : sInstanceMap.values()) { for (final MixpanelAPI instance : contextInstances.values()) { processor.process(instance); } } } } //////////////////////////////////////////////////////////////////// // Conveniences for testing. These methods should not be called by // non-test client code. /* package */ AnalyticsMessages getAnalyticsMessages() { return AnalyticsMessages.getInstance(mContext); } /* package */ PersistentIdentity getPersistentIdentity(final Context context, Future<SharedPreferences> referrerPreferences, final String token) { final SharedPreferencesLoader.OnPrefsLoadedListener listener = new SharedPreferencesLoader.OnPrefsLoadedListener() { @Override public void onPrefsLoaded(SharedPreferences preferences) { final JSONArray records = PersistentIdentity.waitingPeopleRecordsForSending(preferences); if (null != records) { sendAllPeopleRecords(records); } } }; final String prefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI_" + token; final Future<SharedPreferences> storedPreferences = sPrefsLoader.loadPreferences(context, prefsName, listener); final String timeEventsPrefsName = "com.mixpanel.android.mpmetrics.MixpanelAPI.TimeEvents_" + token; final Future<SharedPreferences> timeEventsPrefs = sPrefsLoader.loadPreferences(context, timeEventsPrefsName, null); return new PersistentIdentity(referrerPreferences, storedPreferences, timeEventsPrefs); } /* package */ DecideMessages constructDecideUpdates(final String token, final DecideMessages.OnNewResultsListener listener, UpdatesFromMixpanel updatesFromMixpanel) { return new DecideMessages(token, listener, updatesFromMixpanel); } /* package */ UpdatesListener constructUpdatesListener() { if (Build.VERSION.SDK_INT < MPConfig.UI_FEATURES_MIN_API) { MPLog.i(LOGTAG, "Notifications are not supported on this Android OS Version"); return new UnsupportedUpdatesListener(); } else { return new SupportedUpdatesListener(); } } /* package */ UpdatesFromMixpanel constructUpdatesFromMixpanel(final Context context, final String token) { if (Build.VERSION.SDK_INT < MPConfig.UI_FEATURES_MIN_API) { MPLog.i(LOGTAG, "SDK version is lower than " + MPConfig.UI_FEATURES_MIN_API + ". Web Configuration, A/B Testing, and Dynamic Tweaks are disabled."); return new NoOpUpdatesFromMixpanel(sSharedTweaks); } else if (mConfig.getDisableViewCrawler() || Arrays.asList(mConfig.getDisableViewCrawlerForProjects()).contains(token)) { MPLog.i(LOGTAG, "DisableViewCrawler is set to true. Web Configuration, A/B Testing, and Dynamic Tweaks are disabled."); return new NoOpUpdatesFromMixpanel(sSharedTweaks); } else { return new ViewCrawler(mContext, mToken, this, sSharedTweaks); } } /* package */ TrackingDebug constructTrackingDebug() { if (mUpdatesFromMixpanel instanceof ViewCrawler) { return (TrackingDebug) mUpdatesFromMixpanel; } return null; } /* package */ boolean sendAppOpen() { return !mConfig.getDisableAppOpenEvent(); } /////////////////////// private class PeopleImpl implements People { @Override public void identify(String distinctId) { synchronized (mPersistentIdentity) { mPersistentIdentity.setPeopleDistinctId(distinctId); mDecideMessages.setDistinctId(distinctId); } pushWaitingPeopleRecord(); } @Override public void setMap(Map<String, Object> properties) { if (null == properties) { MPLog.e(LOGTAG, "setMap does not accept null properties"); return; } try { set(new JSONObject(properties)); } catch (NullPointerException e) { MPLog.w(LOGTAG, "Can't have null keys in the properties of setMap!"); } } @Override public void set(JSONObject properties) { try { final JSONObject sendProperties = new JSONObject(mDeviceInfo); for (final Iterator<?> iter = properties.keys(); iter.hasNext();) { final String key = (String) iter.next(); sendProperties.put(key, properties.get(key)); } final JSONObject message = stdPeopleMessage("$set", sendProperties); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception setting people properties", e); } } @Override public void set(String property, Object value) { try { set(new JSONObject().put(property, value)); } catch (final JSONException e) { MPLog.e(LOGTAG, "set", e); } } @Override public void setOnceMap(Map<String, Object> properties) { if (null == properties) { MPLog.e(LOGTAG, "setOnceMap does not accept null properties"); return; } try { setOnce(new JSONObject(properties)); } catch (NullPointerException e) { MPLog.w(LOGTAG, "Can't have null keys in the properties setOnceMap!"); } } @Override public void setOnce(JSONObject properties) { try { final JSONObject message = stdPeopleMessage("$set_once", properties); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception setting people properties"); } } @Override public void setOnce(String property, Object value) { try { setOnce(new JSONObject().put(property, value)); } catch (final JSONException e) { MPLog.e(LOGTAG, "set", e); } } @Override public void increment(Map<String, ? extends Number> properties) { final JSONObject json = new JSONObject(properties); try { final JSONObject message = stdPeopleMessage("$add", json); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception incrementing properties", e); } } @Override // Must be thread safe public void merge(String property, JSONObject updates) { final JSONObject mergeMessage = new JSONObject(); try { mergeMessage.put(property, updates); final JSONObject message = stdPeopleMessage("$merge", mergeMessage); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception merging a property", e); } } @Override public void increment(String property, double value) { final Map<String, Double> map = new HashMap<String, Double>(); map.put(property, value); increment(map); } @Override public void append(String name, Object value) { try { final JSONObject properties = new JSONObject(); properties.put(name, value); final JSONObject message = stdPeopleMessage("$append", properties); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception appending a property", e); } } @Override public void union(String name, JSONArray value) { try { final JSONObject properties = new JSONObject(); properties.put(name, value); final JSONObject message = stdPeopleMessage("$union", properties); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception unioning a property"); } } @Override public void remove(String name, Object value) { try { final JSONObject properties = new JSONObject(); properties.put(name, value); final JSONObject message = stdPeopleMessage("$remove", properties); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception appending a property", e); } } @Override public void unset(String name) { try { final JSONArray names = new JSONArray(); names.put(name); final JSONObject message = stdPeopleMessage("$unset", names); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception unsetting a property", e); } } @Override public InAppNotification getNotificationIfAvailable() { return mDecideMessages.getNotification(mConfig.getTestMode()); } @Override public void trackNotificationSeen(InAppNotification notif) { if(notif == null) return; trackNotification("$campaign_delivery", notif); final MixpanelAPI.People people = getPeople().withIdentity(getDistinctId()); final DateFormat dateFormat = new SimpleDateFormat(ENGAGE_DATE_FORMAT_STRING, Locale.US); final JSONObject notifProperties = notif.getCampaignProperties(); try { notifProperties.put("$time", dateFormat.format(new Date())); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception trying to track an in-app notification seen", e); } people.append("$campaigns", notif.getId()); people.append("$notifications", notifProperties); } @Override public void showNotificationIfAvailable(final Activity parent) { if (Build.VERSION.SDK_INT < MPConfig.UI_FEATURES_MIN_API) { return; } showGivenOrAvailableNotification(null, parent); } @Override public void showNotificationById(int id, final Activity parent) { final InAppNotification notif = mDecideMessages.getNotification(id, mConfig.getTestMode()); showGivenNotification(notif, parent); } @Override public void showGivenNotification(final InAppNotification notif, final Activity parent) { if (notif != null) { showGivenOrAvailableNotification(notif, parent); } } @Override public void trackNotification(final String eventName, final InAppNotification notif) { track(eventName, notif.getCampaignProperties()); } @Override public void joinExperimentIfAvailable() { final JSONArray variants = mDecideMessages.getVariants(); mUpdatesFromMixpanel.setVariants(variants); } @Override public void trackCharge(double amount, JSONObject properties) { final Date now = new Date(); final DateFormat dateFormat = new SimpleDateFormat(ENGAGE_DATE_FORMAT_STRING, Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); try { final JSONObject transactionValue = new JSONObject(); transactionValue.put("$amount", amount); transactionValue.put("$time", dateFormat.format(now)); if (null != properties) { for (final Iterator<?> iter = properties.keys(); iter.hasNext();) { final String key = (String) iter.next(); transactionValue.put(key, properties.get(key)); } } this.append("$transactions", transactionValue); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception creating new charge", e); } } /** * Permanently clear the whole transaction history for the identified people profile. */ @Override public void clearCharges() { this.unset("$transactions"); } @Override public void deleteUser() { try { final JSONObject message = stdPeopleMessage("$delete", JSONObject.NULL); recordPeopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Exception deleting a user"); } } @Override public String getPushRegistrationId() { return mPersistentIdentity.getPushId(); } @Override public void setPushRegistrationId(String registrationId) { // Must be thread safe, will be called from a lot of different threads. synchronized (mPersistentIdentity) { if (mPersistentIdentity.getPeopleDistinctId() == null) { return; } mPersistentIdentity.storePushId(registrationId); final JSONArray ids = new JSONArray(); ids.put(registrationId); union("$android_devices", ids); } } @Override public void clearPushRegistrationId() { mPersistentIdentity.clearPushId(); set("$android_devices", new JSONArray()); } @Override public void clearPushRegistrationId(String registrationId) { if (registrationId == null) { return; } if (registrationId.equals(mPersistentIdentity.getPushId())) { mPersistentIdentity.clearPushId(); } remove("$android_devices", registrationId); } @Override public void initPushHandling(String senderID) { if (! ConfigurationChecker.checkPushConfiguration(mContext) ) { MPLog.i(LOGTAG, "Can't register for push notification services. Push notifications will not work."); MPLog.i(LOGTAG, "See log tagged " + ConfigurationChecker.LOGTAG + " above for details."); } else { // Configuration is good for at least some push notifications final String pushId = mPersistentIdentity.getPushId(); if (pushId == null) { if (Build.VERSION.SDK_INT >= 21) { registerForPushIdAPI21AndUp(senderID); } else { registerForPushIdAPI19AndOlder(senderID); } } else { MixpanelAPI.allInstances(new InstanceProcessor() { @Override public void process(MixpanelAPI api) { MPLog.v(LOGTAG, "Using existing pushId " + pushId); api.getPeople().setPushRegistrationId(pushId); } }); } }// endelse } @Override public String getDistinctId() { return mPersistentIdentity.getPeopleDistinctId(); } @Override public People withIdentity(final String distinctId) { if (null == distinctId) { return null; } return new PeopleImpl() { @Override public String getDistinctId() { return distinctId; } @Override public void identify(String distinctId) { throw new RuntimeException("This MixpanelPeople object has a fixed, constant distinctId"); } }; } @Override public void addOnMixpanelUpdatesReceivedListener(final OnMixpanelUpdatesReceivedListener listener) { mUpdatesListener.addOnMixpanelUpdatesReceivedListener(listener); } @Override public void removeOnMixpanelUpdatesReceivedListener(final OnMixpanelUpdatesReceivedListener listener) { mUpdatesListener.removeOnMixpanelUpdatesReceivedListener(listener); } @Override public void addOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) { if (null == listener) { throw new NullPointerException("Listener cannot be null"); } mUpdatesFromMixpanel.addOnMixpanelTweaksUpdatedListener(listener); } @Override public void removeOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) { mUpdatesFromMixpanel.removeOnMixpanelTweaksUpdatedListener(listener); } private JSONObject stdPeopleMessage(String actionType, Object properties) throws JSONException { final JSONObject dataObj = new JSONObject(); final String distinctId = getDistinctId(); // TODO ensure getDistinctId is thread safe dataObj.put(actionType, properties); dataObj.put("$token", mToken); dataObj.put("$time", System.currentTimeMillis()); if (null != distinctId) { dataObj.put("$distinct_id", distinctId); } return dataObj; } @TargetApi(21) private void registerForPushIdAPI21AndUp(String senderID) { mMessages.registerForGCM(senderID); } @TargetApi(19) private void registerForPushIdAPI19AndOlder(String senderID) { try { MPLog.v(LOGTAG, "Registering a new push id"); final Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER"); registrationIntent.putExtra("app", PendingIntent.getBroadcast(mContext, 0, new Intent(), 0)); registrationIntent.putExtra("sender", senderID); mContext.startService(registrationIntent); } catch (final SecurityException e) { MPLog.w(LOGTAG, "Error registering for push notifications", e); } } private void showGivenOrAvailableNotification(final InAppNotification notifOrNull, final Activity parent) { if (Build.VERSION.SDK_INT < MPConfig.UI_FEATURES_MIN_API) { MPLog.v(LOGTAG, "Will not show notifications, os version is too low."); return; } parent.runOnUiThread(new Runnable() { @Override @TargetApi(MPConfig.UI_FEATURES_MIN_API) public void run() { final ReentrantLock lock = UpdateDisplayState.getLockObject(); lock.lock(); try { if (UpdateDisplayState.hasCurrentProposal()) { MPLog.v(LOGTAG, "DisplayState is locked, will not show notifications."); return; // Already being used. } InAppNotification toShow = notifOrNull; if (null == toShow) { toShow = getNotificationIfAvailable(); } if (null == toShow) { MPLog.v(LOGTAG, "No notification available, will not show."); return; // Nothing to show } final InAppNotification.Type inAppType = toShow.getType(); if (inAppType == InAppNotification.Type.TAKEOVER && !ConfigurationChecker.checkTakeoverInAppActivityAvailable(parent.getApplicationContext())) { MPLog.v(LOGTAG, "Application is not configured to show takeover notifications, none will be shown."); return; // Can't show due to config. } final int highlightColor = ActivityImageUtils.getHighlightColorFromBackground(parent); final UpdateDisplayState.DisplayState.InAppNotificationState proposal = new UpdateDisplayState.DisplayState.InAppNotificationState(toShow, highlightColor); final int intentId = UpdateDisplayState.proposeDisplay(proposal, getDistinctId(), mToken); if (intentId <= 0) { MPLog.e(LOGTAG, "DisplayState Lock in inconsistent state! Please report this issue to Mixpanel"); return; } switch (inAppType) { case MINI: { final UpdateDisplayState claimed = UpdateDisplayState.claimDisplayState(intentId); if (null == claimed) { MPLog.v(LOGTAG, "Notification's display proposal was already consumed, no notification will be shown."); return; // Can't claim the display state } final InAppFragment inapp = new InAppFragment(); inapp.setDisplayState( MixpanelAPI.this, intentId, (UpdateDisplayState.DisplayState.InAppNotificationState) claimed.getDisplayState() ); inapp.setRetainInstance(true); MPLog.v(LOGTAG, "Attempting to show mini notification."); final FragmentTransaction transaction = parent.getFragmentManager().beginTransaction(); transaction.setCustomAnimations(0, R.animator.com_mixpanel_android_slide_down); transaction.add(android.R.id.content, inapp); try { transaction.commit(); } catch (IllegalStateException e) { // if the app is in the background or the current activity gets killed, rendering the // notifiction will lead to a crash MPLog.v(LOGTAG, "Unable to show notification."); mDecideMessages.markNotificationAsUnseen(toShow); } } break; case TAKEOVER: { MPLog.v(LOGTAG, "Sending intent for takeover notification."); final Intent intent = new Intent(parent.getApplicationContext(), TakeoverInAppActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); intent.putExtra(TakeoverInAppActivity.INTENT_ID_KEY, intentId); parent.startActivity(intent); } break; default: MPLog.e(LOGTAG, "Unrecognized notification type " + inAppType + " can't be shown"); } if (!mConfig.getTestMode()) { trackNotificationSeen(toShow); } } finally { lock.unlock(); } } // run() }); } }// PeopleImpl private interface UpdatesListener extends DecideMessages.OnNewResultsListener { public void addOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener); public void removeOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener); } private class UnsupportedUpdatesListener implements UpdatesListener { @Override public void onNewResults() { // Do nothing, these features aren't supported in older versions of the library } @Override public void addOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener) { // Do nothing, not supported } @Override public void removeOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener) { // Do nothing, not supported } } private class SupportedUpdatesListener implements UpdatesListener, Runnable { @Override public void onNewResults() { mExecutor.execute(this); } @Override public void addOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener) { mListeners.add(listener); if (mDecideMessages.hasUpdatesAvailable()) { onNewResults(); } } @Override public void removeOnMixpanelUpdatesReceivedListener(OnMixpanelUpdatesReceivedListener listener) { mListeners.remove(listener); } @Override public void run() { // It's possible that by the time this has run the updates we detected are no longer // present, which is ok. for (final OnMixpanelUpdatesReceivedListener listener : mListeners) { listener.onMixpanelUpdatesReceived(); } } private final Set<OnMixpanelUpdatesReceivedListener> mListeners = Collections.newSetFromMap(new ConcurrentHashMap<OnMixpanelUpdatesReceivedListener, Boolean>()); private final Executor mExecutor = Executors.newSingleThreadExecutor(); } /* package */ class NoOpUpdatesFromMixpanel implements UpdatesFromMixpanel { public NoOpUpdatesFromMixpanel(Tweaks tweaks) { mTweaks = tweaks; } @Override public void startUpdates() { // No op } @Override public void setEventBindings(JSONArray bindings) { // No op } @Override public void setVariants(JSONArray variants) { // No op } @Override public Tweaks getTweaks() { return mTweaks; } @Override public void addOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) { // No op } @Override public void removeOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) { // No op } private final Tweaks mTweaks; } //////////////////////////////////////////////////// private void recordPeopleMessage(JSONObject message) { if (message.has("$distinct_id")) { mMessages.peopleMessage(message); } else { mPersistentIdentity.storeWaitingPeopleRecord(message); } } private void pushWaitingPeopleRecord() { final JSONArray records = mPersistentIdentity.waitingPeopleRecordsForSending(); if (null != records) { sendAllPeopleRecords(records); } } // MUST BE THREAD SAFE. Called from crazy places. mPersistentIdentity may not exist // when this is called (from its crazy thread) private void sendAllPeopleRecords(JSONArray records) { for (int i = 0; i < records.length(); i++) { try { final JSONObject message = records.getJSONObject(i); mMessages.peopleMessage(message); } catch (final JSONException e) { MPLog.e(LOGTAG, "Malformed people record stored pending identity, will not send it.", e); } } } private static void registerAppLinksListeners(Context context, final MixpanelAPI mixpanel) { // Register a BroadcastReceiver to receive com.parse.bolts.measurement_event and track a call to mixpanel try { final Class<?> clazz = Class.forName("android.support.v4.content.LocalBroadcastManager"); final Method methodGetInstance = clazz.getMethod("getInstance", Context.class); final Method methodRegisterReceiver = clazz.getMethod("registerReceiver", BroadcastReceiver.class, IntentFilter.class); final Object localBroadcastManager = methodGetInstance.invoke(null, context); methodRegisterReceiver.invoke(localBroadcastManager, new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final JSONObject properties = new JSONObject(); final Bundle args = intent.getBundleExtra("event_args"); if (args != null) { for (final String key : args.keySet()) { try { properties.put(key, args.get(key)); } catch (final JSONException e) { MPLog.e(APP_LINKS_LOGTAG, "failed to add key \"" + key + "\" to properties for tracking bolts event", e); } } } mixpanel.track("$" + intent.getStringExtra("event_name"), properties); } }, new IntentFilter("com.parse.bolts.measurement_event")); } catch (final InvocationTargetException e) { MPLog.d(APP_LINKS_LOGTAG, "Failed to invoke LocalBroadcastManager.registerReceiver() -- App Links tracking will not be enabled due to this exception", e); } catch (final ClassNotFoundException e) { MPLog.d(APP_LINKS_LOGTAG, "To enable App Links tracking android.support.v4 must be installed: " + e.getMessage()); } catch (final NoSuchMethodException e) { MPLog.d(APP_LINKS_LOGTAG, "To enable App Links tracking android.support.v4 must be installed: " + e.getMessage()); } catch (final IllegalAccessException e) { MPLog.d(APP_LINKS_LOGTAG, "App Links tracking will not be enabled due to this exception: " + e.getMessage()); } } private static void checkIntentForInboundAppLink(Context context) { // call the Bolts getTargetUrlFromInboundIntent method simply for a side effect // if the intent is the result of an App Link, it'll trigger al_nav_in // https://github.com/BoltsFramework/Bolts-Android/blob/1.1.2/Bolts/src/bolts/AppLinks.java#L86 if (context instanceof Activity) { try { final Class<?> clazz = Class.forName("bolts.AppLinks"); final Intent intent = ((Activity) context).getIntent(); final Method getTargetUrlFromInboundIntent = clazz.getMethod("getTargetUrlFromInboundIntent", Context.class, Intent.class); getTargetUrlFromInboundIntent.invoke(null, context, intent); } catch (final InvocationTargetException e) { MPLog.d(APP_LINKS_LOGTAG, "Failed to invoke bolts.AppLinks.getTargetUrlFromInboundIntent() -- Unable to detect inbound App Links", e); } catch (final ClassNotFoundException e) { MPLog.d(APP_LINKS_LOGTAG, "Please install the Bolts library >= 1.1.2 to track App Links: " + e.getMessage()); } catch (final NoSuchMethodException e) { MPLog.d(APP_LINKS_LOGTAG, "Please install the Bolts library >= 1.1.2 to track App Links: " + e.getMessage()); } catch (final IllegalAccessException e) { MPLog.d(APP_LINKS_LOGTAG, "Unable to detect inbound App Links: " + e.getMessage()); } } else { MPLog.d(APP_LINKS_LOGTAG, "Context is not an instance of Activity. To detect inbound App Links, pass an instance of an Activity to getInstance."); } } private final Context mContext; private final AnalyticsMessages mMessages; private final MPConfig mConfig; private final String mToken; private final PeopleImpl mPeople; private final UpdatesFromMixpanel mUpdatesFromMixpanel; private final PersistentIdentity mPersistentIdentity; private final UpdatesListener mUpdatesListener; private final TrackingDebug mTrackingDebug; private final DecideMessages mDecideMessages; private final Map<String, String> mDeviceInfo; private final Map<String, Long> mEventTimings; private MixpanelActivityLifecycleCallbacks mMixpanelActivityLifecycleCallbacks; // Maps each token to a singleton MixpanelAPI instance private static final Map<String, Map<Context, MixpanelAPI>> sInstanceMap = new HashMap<String, Map<Context, MixpanelAPI>>(); private static final SharedPreferencesLoader sPrefsLoader = new SharedPreferencesLoader(); private static final Tweaks sSharedTweaks = new Tweaks(); private static Future<SharedPreferences> sReferrerPrefs; private static final String LOGTAG = "MixpanelAPI.API"; private static final String APP_LINKS_LOGTAG = "MixpanelAPI.AL"; private static final String ENGAGE_DATE_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss"; }