package com.mixpanel.android.mpmetrics;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import com.mixpanel.android.util.MPLog;
// In order to use writeEdits, we have to suppress the linter's check for commit()/apply()
@SuppressLint("CommitPrefEdits")
/* package */ class PersistentIdentity {
// Should ONLY be called from an OnPrefsLoadedListener (since it should NEVER be called concurrently)
public static JSONArray waitingPeopleRecordsForSending(SharedPreferences storedPreferences) {
JSONArray ret = null;
final String peopleDistinctId = storedPreferences.getString("people_distinct_id", null);
final String waitingPeopleRecords = storedPreferences.getString("waiting_array", null);
if ((null != waitingPeopleRecords) && (null != peopleDistinctId)) {
JSONArray waitingObjects = null;
try {
waitingObjects = new JSONArray(waitingPeopleRecords);
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Waiting people records were unreadable.");
return null;
}
ret = new JSONArray();
for (int i = 0; i < waitingObjects.length(); i++) {
try {
final JSONObject ob = waitingObjects.getJSONObject(i);
ob.put("$distinct_id", peopleDistinctId);
ret.put(ob);
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Unparsable object found in waiting people records", e);
}
}
final SharedPreferences.Editor editor = storedPreferences.edit();
editor.remove("waiting_array");
writeEdits(editor);
}
return ret;
}
public static void writeReferrerPrefs(Context context, String preferencesName, Map<String, String> properties) {
synchronized (sReferrerPrefsLock) {
final SharedPreferences referralInfo = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE);
final SharedPreferences.Editor editor = referralInfo.edit();
editor.clear();
for (final Map.Entry<String, String> entry : properties.entrySet()) {
editor.putString(entry.getKey(), entry.getValue());
}
writeEdits(editor);
sReferrerPrefsDirty = true;
}
}
public PersistentIdentity(Future<SharedPreferences> referrerPreferences, Future<SharedPreferences> storedPreferences, Future<SharedPreferences> timeEventsPreferences) {
mLoadReferrerPreferences = referrerPreferences;
mLoadStoredPreferences = storedPreferences;
mTimeEventsPreferences = timeEventsPreferences;
mSuperPropertiesCache = null;
mReferrerPropertiesCache = null;
mIdentitiesLoaded = false;
mReferrerChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
synchronized (sReferrerPrefsLock) {
readReferrerProperties();
sReferrerPrefsDirty = false;
}
}
};
}
public synchronized void addSuperPropertiesToObject(JSONObject ob) {
final JSONObject superProperties = this.getSuperPropertiesCache();
final Iterator<?> superIter = superProperties.keys();
while (superIter.hasNext()) {
final String key = (String) superIter.next();
try {
ob.put(key, superProperties.get(key));
} catch (JSONException e) {
MPLog.e(LOGTAG, "Object read from one JSON Object cannot be written to another", e);
}
}
}
public synchronized void updateSuperProperties(SuperPropertyUpdate updates) {
final JSONObject oldPropCache = getSuperPropertiesCache();
final JSONObject copy = new JSONObject();
try {
final Iterator<String> keys = oldPropCache.keys();
while (keys.hasNext()) {
final String k = keys.next();
final Object v = oldPropCache.get(k);
copy.put(k, v);
}
} catch (JSONException e) {
MPLog.e(LOGTAG, "Can't copy from one JSONObject to another", e);
return;
}
final JSONObject replacementCache = updates.update(copy);
if (null == replacementCache) {
MPLog.w(LOGTAG, "An update to Mixpanel's super properties returned null, and will have no effect.");
return;
}
mSuperPropertiesCache = replacementCache;
storeSuperProperties();
}
public Map<String, String> getReferrerProperties() {
synchronized (sReferrerPrefsLock) {
if (sReferrerPrefsDirty || null == mReferrerPropertiesCache) {
readReferrerProperties();
sReferrerPrefsDirty = false;
}
}
return mReferrerPropertiesCache;
}
public synchronized String getEventsDistinctId() {
if (! mIdentitiesLoaded) {
readIdentities();
}
return mEventsDistinctId;
}
public synchronized void setEventsDistinctId(String eventsDistinctId) {
if (! mIdentitiesLoaded) {
readIdentities();
}
mEventsDistinctId = eventsDistinctId;
writeIdentities();
}
public synchronized String getPeopleDistinctId() {
if (! mIdentitiesLoaded) {
readIdentities();
}
return mPeopleDistinctId;
}
public synchronized void setPeopleDistinctId(String peopleDistinctId) {
if (! mIdentitiesLoaded) {
readIdentities();
}
mPeopleDistinctId = peopleDistinctId;
writeIdentities();
}
public synchronized boolean hasTrackedIntegration() {
if (! mIdentitiesLoaded) {
readIdentities();
}
return mTrackedIntegration;
}
public synchronized void setTrackedIntegration(boolean trackedIntegration) {
if (! mIdentitiesLoaded) {
readIdentities();
}
mTrackedIntegration = trackedIntegration;
writeIdentities();
}
public synchronized void storeWaitingPeopleRecord(JSONObject record) {
if (! mIdentitiesLoaded) {
readIdentities();
}
if (null == mWaitingPeopleRecords) {
mWaitingPeopleRecords = new JSONArray();
}
mWaitingPeopleRecords.put(record);
writeIdentities();
}
public synchronized JSONArray waitingPeopleRecordsForSending() {
JSONArray ret = null;
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
ret = waitingPeopleRecordsForSending(prefs);
readIdentities();
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Couldn't read waiting people records from shared preferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Couldn't read waiting people records from shared preferences.", e);
}
return ret;
}
public synchronized void clearPreferences() {
// Will clear distinct_ids, superProperties,
// and waiting People Analytics properties. Will have no effect
// on messages already queued to send with AnalyticsMessages.
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final SharedPreferences.Editor prefsEdit = prefs.edit();
prefsEdit.clear();
writeEdits(prefsEdit);
readSuperProperties();
readIdentities();
} catch (final ExecutionException e) {
throw new RuntimeException(e.getCause());
} catch (final InterruptedException e) {
throw new RuntimeException(e.getCause());
}
}
public synchronized void registerSuperProperties(JSONObject superProperties) {
final JSONObject propCache = getSuperPropertiesCache();
for (final Iterator<?> iter = superProperties.keys(); iter.hasNext(); ) {
final String key = (String) iter.next();
try {
propCache.put(key, superProperties.get(key));
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Exception registering super property.", e);
}
}
storeSuperProperties();
}
public synchronized void storePushId(String registrationId) {
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final SharedPreferences.Editor editor = prefs.edit();
editor.putString("push_id", registrationId);
writeEdits(editor);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e);
}
}
public synchronized void clearPushId() {
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final SharedPreferences.Editor editor = prefs.edit();
editor.remove("push_id");
writeEdits(editor);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e);
}
}
public synchronized String getPushId() {
String ret = null;
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
ret = prefs.getString("push_id", null);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Can't write push id to shared preferences", e);
}
return ret;
}
public synchronized void unregisterSuperProperty(String superPropertyName) {
final JSONObject propCache = getSuperPropertiesCache();
propCache.remove(superPropertyName);
storeSuperProperties();
}
public Map<String, Long> getTimeEvents() {
Map<String, Long> timeEvents = new HashMap<>();
try {
final SharedPreferences prefs = mTimeEventsPreferences.get();
Map<String, ?> allEntries = prefs.getAll();
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
timeEvents.put(entry.getKey(), Long.valueOf(entry.getValue().toString()));
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return timeEvents;
}
// access is synchronized outside (mEventTimings)
public void removeTimeEvent(String timeEventName) {
try {
final SharedPreferences prefs = mTimeEventsPreferences.get();
final SharedPreferences.Editor editor = prefs.edit();
editor.remove(timeEventName);
writeEdits(editor);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// access is synchronized outside (mEventTimings)
public void addTimeEvent(String timeEventName, Long timeEventTimestamp) {
try {
final SharedPreferences prefs = mTimeEventsPreferences.get();
final SharedPreferences.Editor editor = prefs.edit();
editor.putLong(timeEventName, timeEventTimestamp);
writeEdits(editor);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
public synchronized void registerSuperPropertiesOnce(JSONObject superProperties) {
final JSONObject propCache = getSuperPropertiesCache();
for (final Iterator<?> iter = superProperties.keys(); iter.hasNext(); ) {
final String key = (String) iter.next();
if (! propCache.has(key)) {
try {
propCache.put(key, superProperties.get(key));
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Exception registering super property.", e);
}
}
}// for
storeSuperProperties();
}
public synchronized void clearSuperProperties() {
mSuperPropertiesCache = new JSONObject();
storeSuperProperties();
}
//////////////////////////////////////////////////
// Must be called from a synchronized setting
private JSONObject getSuperPropertiesCache() {
if (null == mSuperPropertiesCache) {
readSuperProperties();
}
return mSuperPropertiesCache;
}
// All access should be synchronized on this
private void readSuperProperties() {
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final String props = prefs.getString("super_properties", "{}");
MPLog.v(LOGTAG, "Loading Super Properties " + props);
mSuperPropertiesCache = new JSONObject(props);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Cannot load superProperties from SharedPreferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Cannot load superProperties from SharedPreferences.", e);
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Cannot parse stored superProperties");
storeSuperProperties();
} finally {
if (null == mSuperPropertiesCache) {
mSuperPropertiesCache = new JSONObject();
}
}
}
// All access should be synchronized on this
private void readReferrerProperties() {
mReferrerPropertiesCache = new HashMap<String, String>();
try {
final SharedPreferences referrerPrefs = mLoadReferrerPreferences.get();
referrerPrefs.unregisterOnSharedPreferenceChangeListener(mReferrerChangeListener);
referrerPrefs.registerOnSharedPreferenceChangeListener(mReferrerChangeListener);
final Map<String, ?> prefsMap = referrerPrefs.getAll();
for (final Map.Entry<String, ?> entry : prefsMap.entrySet()) {
final String prefsName = entry.getKey();
final Object prefsVal = entry.getValue();
mReferrerPropertiesCache.put(prefsName, prefsVal.toString());
}
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Cannot load referrer properties from shared preferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Cannot load referrer properties from shared preferences.", e);
}
}
// All access should be synchronized on this
private void storeSuperProperties() {
if (null == mSuperPropertiesCache) {
MPLog.e(LOGTAG, "storeSuperProperties should not be called with uninitialized superPropertiesCache.");
return;
}
final String props = mSuperPropertiesCache.toString();
MPLog.v(LOGTAG, "Storing Super Properties " + props);
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final SharedPreferences.Editor editor = prefs.edit();
editor.putString("super_properties", props);
writeEdits(editor);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Cannot store superProperties in shared preferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Cannot store superProperties in shared preferences.", e);
}
}
// All access should be synchronized on this
private void readIdentities() {
SharedPreferences prefs = null;
try {
prefs = mLoadStoredPreferences.get();
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Cannot read distinct ids from sharedPreferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Cannot read distinct ids from sharedPreferences.", e);
}
if (null == prefs) {
return;
}
mEventsDistinctId = prefs.getString("events_distinct_id", null);
mPeopleDistinctId = prefs.getString("people_distinct_id", null);
mTrackedIntegration = prefs.getBoolean("tracked_integration", false);
mWaitingPeopleRecords = null;
final String storedWaitingRecord = prefs.getString("waiting_array", null);
if (storedWaitingRecord != null) {
try {
mWaitingPeopleRecords = new JSONArray(storedWaitingRecord);
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Could not interpret waiting people JSON record " + storedWaitingRecord);
}
}
if (null == mEventsDistinctId) {
mEventsDistinctId = UUID.randomUUID().toString();
writeIdentities();
}
mIdentitiesLoaded = true;
}
// All access should be synchronized on this
private void writeIdentities() {
try {
final SharedPreferences prefs = mLoadStoredPreferences.get();
final SharedPreferences.Editor prefsEditor = prefs.edit();
prefsEditor.putString("events_distinct_id", mEventsDistinctId);
prefsEditor.putString("people_distinct_id", mPeopleDistinctId);
if (mWaitingPeopleRecords == null) {
prefsEditor.remove("waiting_array");
} else {
prefsEditor.putString("waiting_array", mWaitingPeopleRecords.toString());
}
prefsEditor.putBoolean("tracked_integration", mTrackedIntegration);
writeEdits(prefsEditor);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Can't write distinct ids to shared preferences.", e.getCause());
} catch (final InterruptedException e) {
MPLog.e(LOGTAG, "Can't write distinct ids to shared preferences.", e);
}
}
private static void writeEdits(final SharedPreferences.Editor editor) {
editor.apply();
}
private final Future<SharedPreferences> mLoadStoredPreferences;
private final Future<SharedPreferences> mLoadReferrerPreferences;
private final Future<SharedPreferences> mTimeEventsPreferences;
private final SharedPreferences.OnSharedPreferenceChangeListener mReferrerChangeListener;
private JSONObject mSuperPropertiesCache;
private Map<String, String> mReferrerPropertiesCache;
private boolean mIdentitiesLoaded;
private String mEventsDistinctId;
private String mPeopleDistinctId;
private boolean mTrackedIntegration;
private JSONArray mWaitingPeopleRecords;
private static boolean sReferrerPrefsDirty = true;
private static final Object sReferrerPrefsLock = new Object();
private static final String LOGTAG = "MixpanelAPI.PIdentity";
}