package com.mixpanel.android.mpmetrics; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import com.mixpanel.android.util.MPLog; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; /** * This is a class intended for internal use by the library. * Users of the library should not interact with it directly. * * The interface to this class may change or disappear from release to release. */ @TargetApi(MPConfig.UI_FEATURES_MIN_API) public class UpdateDisplayState implements Parcelable { /** * This class is intended for internal use by the Mixpanel library. * Users of the library should not interact directly with this class. */ public static abstract class DisplayState implements Parcelable { private DisplayState() {} public abstract String getType(); /** * This class is intended for internal use by the Mixpanel library. * Users of the library should not interact directly with this class. */ public static final class InAppNotificationState extends DisplayState { public static final String TYPE = "InAppNotificationState"; public static final Creator<InAppNotificationState> CREATOR = new Creator<InAppNotificationState>() { @Override public InAppNotificationState createFromParcel(final Parcel source) { final Bundle read = new Bundle(InAppNotificationState.class.getClassLoader()); read.readFromParcel(source); return new InAppNotificationState(read); } @Override public InAppNotificationState[] newArray(final int size) { return new InAppNotificationState[size]; } }; public InAppNotificationState(InAppNotification notification, int highlightColor) { mInAppNotification = notification; mHighlightColor = highlightColor; } public int getHighlightColor() { return mHighlightColor; } public InAppNotification getInAppNotification() { return mInAppNotification; } @Override public String getType() { return TYPE; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(final Parcel dest, final int flags) { final Bundle write = new Bundle(); write.putParcelable(INAPP_KEY, mInAppNotification); write.putInt(HIGHLIGHT_KEY, mHighlightColor); dest.writeBundle(write); } private InAppNotificationState(Bundle in) { mInAppNotification = in.getParcelable(INAPP_KEY); mHighlightColor = in.getInt(HIGHLIGHT_KEY); } private final InAppNotification mInAppNotification; private final int mHighlightColor; private static String INAPP_KEY = "com.com.mixpanel.android.mpmetrics.UpdateDisplayState.InAppNotificationState.INAPP_KEY"; private static String HIGHLIGHT_KEY = "com.com.mixpanel.android.mpmetrics.UpdateDisplayState.InAppNotificationState.HIGHLIGHT_KEY"; } public static final Creator<DisplayState> CREATOR = new Creator<DisplayState>() { @Override public DisplayState createFromParcel(final Parcel source) { final Bundle read = new Bundle(DisplayState.class.getClassLoader()); read.readFromParcel(source); final String type = read.getString(STATE_TYPE_KEY); final Bundle implementation = read.getBundle(STATE_IMPL_KEY); if (InAppNotificationState.TYPE.equals(type)) { return new InAppNotificationState(implementation); } else { throw new RuntimeException("Unrecognized display state type " + type); } } @Override public DisplayState[] newArray(final int size) { return new DisplayState[size]; } }; public static final String STATE_TYPE_KEY = "com.mixpanel.android.mpmetrics.UpdateDisplayState.DisplayState.STATE_TYPE_KEY"; public static final String STATE_IMPL_KEY = "com.mixpanel.android.mpmetrics.UpdateDisplayState.DisplayState.STATE_IMPL_KEY"; } /** * This class is intended for internal use by the Mixpanel library. * Users of the library should not interact directly with this class. */ public static class AnswerMap implements Parcelable { @SuppressLint("UseSparseArrays") public AnswerMap() { mMap = new HashMap<Integer, String>(); } public void put(Integer i, String s) { mMap.put(i, s); } public String get(Integer i) { return mMap.get(i); } public boolean contentEquals(AnswerMap other) { return mMap.equals(other.mMap); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { final Bundle out = new Bundle(); for (final Map.Entry<Integer, String> entry : mMap.entrySet()) { final String keyString = Integer.toString(entry.getKey()); out.putString(keyString, entry.getValue()); } dest.writeBundle(out); } public static final Parcelable.Creator<AnswerMap> CREATOR = new Parcelable.Creator<AnswerMap>() { @Override public AnswerMap createFromParcel(Parcel in) { final Bundle read = new Bundle(AnswerMap.class.getClassLoader()); final AnswerMap ret = new AnswerMap(); read.readFromParcel(in); for (final String kString : read.keySet()) { final Integer kInt = Integer.valueOf(kString); ret.put(kInt, read.getString(kString)); } return ret; } @Override public AnswerMap[] newArray(int size) { return new AnswerMap[size]; } }; private final HashMap<Integer, String> mMap; } /* package */ static ReentrantLock getLockObject() { // Returns an unlocked lock object. Does *not* acquire a lock! return sUpdateDisplayLock; } /* package */ static boolean hasCurrentProposal() { // Almost certainly a race condition of caller doesn't hold our lock object. if (!sUpdateDisplayLock.isHeldByCurrentThread()) throw new AssertionError(); final long currentTime = System.currentTimeMillis(); final long deltaTime = currentTime - sUpdateDisplayLockMillis; if (sNextIntentId > 0 && deltaTime > MAX_LOCK_TIME_MILLIS) { MPLog.i(LOGTAG, "UpdateDisplayState set long, long ago, without showing. Update state will be cleared."); sUpdateDisplayState = null; } return null != sUpdateDisplayState; } // Returned id should either be -1, or POSITIVE (nonzero). Don't return zero, please. /* package */ static int proposeDisplay(final DisplayState state, final String distinctId, final String token) { int ret = -1; if (!sUpdateDisplayLock.isHeldByCurrentThread()) throw new AssertionError(); if (! hasCurrentProposal()) { sUpdateDisplayLockMillis = System.currentTimeMillis(); sUpdateDisplayState = new UpdateDisplayState(state, distinctId, token); sNextIntentId++; ret = sNextIntentId; } else { MPLog.v(LOGTAG, "Already showing (or cooking) a Mixpanel update, declining to show another."); } return ret; } /** * Client code should not call this method. */ public static void releaseDisplayState(int intentId) { sUpdateDisplayLock.lock(); try { if (intentId == sShowingIntentId) { sShowingIntentId = -1; sUpdateDisplayState = null; } } finally { sUpdateDisplayLock.unlock(); } } /** * Client code should not call this method. */ public static UpdateDisplayState claimDisplayState(final int intentId) { sUpdateDisplayLock.lock(); try { if (sShowingIntentId > 0 && sShowingIntentId != intentId) { // Someone else has claimed another intent already return null; } else if (sUpdateDisplayState == null) { // Nothing to claim, caller is too late (or crazy) return null; } else { // Claim is successful sUpdateDisplayLockMillis = System.currentTimeMillis(); sShowingIntentId = intentId; return sUpdateDisplayState; } } finally { sUpdateDisplayLock.unlock(); } } public static final Parcelable.Creator<UpdateDisplayState> CREATOR = new Parcelable.Creator<UpdateDisplayState>() { @Override public UpdateDisplayState createFromParcel(Parcel in) { final Bundle read = new Bundle(UpdateDisplayState.class.getClassLoader()); read.readFromParcel(in); return new UpdateDisplayState(read); } @Override public UpdateDisplayState[] newArray(int size) { return new UpdateDisplayState[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { final Bundle bundle = new Bundle(); bundle.putString(DISTINCT_ID_BUNDLE_KEY, mDistinctId); bundle.putString(TOKEN_BUNDLE_KEY, mToken); bundle.putParcelable(DISPLAYSTATE_BUNDLE_KEY, mDisplayState); dest.writeBundle(bundle); } public DisplayState getDisplayState() { return mDisplayState; } public String getDistinctId() { return mDistinctId; } public String getToken() { return mToken; } // Package access for testing only- DO NOT CALL in library code /* package */ UpdateDisplayState(final DisplayState displayState, final String distinctId, final String token) { mDistinctId = distinctId; mToken = token; mDisplayState = displayState; } // Bundle must have the same ClassLoader that loaded this constructor. private UpdateDisplayState(Bundle read) { mDistinctId = read.getString(DISTINCT_ID_BUNDLE_KEY); mToken = read.getString(TOKEN_BUNDLE_KEY); mDisplayState = read.getParcelable(DISPLAYSTATE_BUNDLE_KEY); } private final String mDistinctId; private final String mToken; private final DisplayState mDisplayState; private static final ReentrantLock sUpdateDisplayLock = new ReentrantLock(); private static long sUpdateDisplayLockMillis = -1; private static UpdateDisplayState sUpdateDisplayState = null; private static int sNextIntentId = 0; private static int sShowingIntentId = -1; private static final String LOGTAG = "MixpanelAPI.UpDisplSt"; private static final long MAX_LOCK_TIME_MILLIS = 12 * 60 * 60 * 1000; // Twelve hour timeout on inApp activities private static final String DISTINCT_ID_BUNDLE_KEY = "com.mixpanel.android.mpmetrics.UpdateDisplayState.DISTINCT_ID_BUNDLE_KEY"; private static final String TOKEN_BUNDLE_KEY = "com.mixpanel.android.mpmetrics.UpdateDisplayState.TOKEN_BUNDLE_KEY"; private static final String DISPLAYSTATE_BUNDLE_KEY = "com.mixpanel.android.mpmetrics.UpdateDisplayState.DISPLAYSTATE_BUNDLE_KEY"; }