package com.mixpanel.android.viewcrawler;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.util.JsonWriter;
import android.util.Pair;
import com.mixpanel.android.mpmetrics.MPConfig;
import com.mixpanel.android.mpmetrics.MixpanelAPI;
import com.mixpanel.android.mpmetrics.OnMixpanelTweaksUpdatedListener;
import com.mixpanel.android.mpmetrics.ResourceIds;
import com.mixpanel.android.mpmetrics.ResourceReader;
import com.mixpanel.android.mpmetrics.SuperPropertyUpdate;
import com.mixpanel.android.mpmetrics.Tweaks;
import com.mixpanel.android.util.ImageStore;
import com.mixpanel.android.util.JSONUtils;
import com.mixpanel.android.util.MPLog;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.SSLSocketFactory;
/**
* This class is for internal use by the Mixpanel API, and should
* not be called directly by your code.
*/
@TargetApi(MPConfig.UI_FEATURES_MIN_API)
public class ViewCrawler implements UpdatesFromMixpanel, TrackingDebug, ViewVisitor.OnLayoutErrorListener {
public ViewCrawler(Context context, String token, MixpanelAPI mixpanel, Tweaks tweaks) {
mConfig = MPConfig.getInstance(context);
mContext = context;
mEditState = new EditState();
mTweaks = tweaks;
mDeviceInfo = mixpanel.getDeviceInfo();
mScaledDensity = Resources.getSystem().getDisplayMetrics().scaledDensity;
mTweaksUpdatedListeners = Collections.newSetFromMap(new ConcurrentHashMap<OnMixpanelTweaksUpdatedListener, Boolean>());
final Application app = (Application) context.getApplicationContext();
app.registerActivityLifecycleCallbacks(new LifecycleCallbacks());
final HandlerThread thread = new HandlerThread(ViewCrawler.class.getCanonicalName());
thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
mMessageThreadHandler = new ViewCrawlerHandler(context, token, thread.getLooper(), this);
mDynamicEventTracker = new DynamicEventTracker(mixpanel, mMessageThreadHandler);
mMixpanel = mixpanel;
mTweaks.addOnTweakDeclaredListener(new Tweaks.OnTweakDeclaredListener() {
@Override
public void onTweakDeclared() {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_DEVICE_INFO);
mMessageThreadHandler.sendMessage(msg);
}
});
}
@Override
public void startUpdates() {
mMessageThreadHandler.start();
mMessageThreadHandler.sendMessage(mMessageThreadHandler.obtainMessage(MESSAGE_INITIALIZE_CHANGES));
}
@Override
public Tweaks getTweaks() {
return mTweaks;
}
@Override
public void setEventBindings(JSONArray bindings) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_EVENT_BINDINGS_RECEIVED);
msg.obj = bindings;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void setVariants(JSONArray variants) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_VARIANTS_RECEIVED);
msg.obj = variants;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void addOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) {
if (null == listener) {
throw new NullPointerException("Listener cannot be null");
}
mTweaksUpdatedListeners.add(listener);
}
@Override
public void removeOnMixpanelTweaksUpdatedListener(OnMixpanelTweaksUpdatedListener listener) {
mTweaksUpdatedListeners.remove(listener);
}
@Override
public void reportTrack(String eventName) {
final Message m = mMessageThreadHandler.obtainMessage();
m.what = MESSAGE_SEND_EVENT_TRACKED;
m.obj = eventName;
mMessageThreadHandler.sendMessage(m);
}
@Override
public void onLayoutError(ViewVisitor.LayoutErrorMessage e) {
final Message m = mMessageThreadHandler.obtainMessage();
m.what = MESSAGE_SEND_LAYOUT_ERROR;
m.obj = e;
mMessageThreadHandler.sendMessage(m);
}
private class EmulatorConnector implements Runnable {
public EmulatorConnector() {
mStopped = true;
}
@Override
public void run() {
if (! mStopped) {
final Message message = mMessageThreadHandler.obtainMessage(MESSAGE_CONNECT_TO_EDITOR);
mMessageThreadHandler.sendMessage(message);
}
mMessageThreadHandler.postDelayed(this, EMULATOR_CONNECT_ATTEMPT_INTERVAL_MILLIS);
}
public void start() {
mStopped = false;
mMessageThreadHandler.post(this);
}
public void stop() {
mStopped = true;
mMessageThreadHandler.removeCallbacks(this);
}
private volatile boolean mStopped;
}
private class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks, FlipGesture.OnFlipGestureListener {
public LifecycleCallbacks() {
mFlipGesture = new FlipGesture(this);
mEmulatorConnector = new EmulatorConnector();
}
@Override
public void onFlipGesture() {
mMixpanel.track("$ab_gesture3");
final Message message = mMessageThreadHandler.obtainMessage(MESSAGE_CONNECT_TO_EDITOR);
mMessageThreadHandler.sendMessage(message);
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
installConnectionSensor(activity);
mEditState.add(activity);
}
@Override
public void onActivityPaused(Activity activity) {
mEditState.remove(activity);
uninstallConnectionSensor(activity);
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
private void installConnectionSensor(final Activity activity) {
if (isInEmulator() && !mConfig.getDisableEmulatorBindingUI()) {
mEmulatorConnector.start();
} else if (!mConfig.getDisableGestureBindingUI()) {
final SensorManager sensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
final Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(mFlipGesture, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}
}
private void uninstallConnectionSensor(final Activity activity) {
if (isInEmulator() && !mConfig.getDisableEmulatorBindingUI()) {
mEmulatorConnector.stop();
} else if (!mConfig.getDisableGestureBindingUI()) {
final SensorManager sensorManager = (SensorManager) activity.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(mFlipGesture);
}
}
private boolean isInEmulator() {
if (!Build.HARDWARE.equals("goldfish") && !Build.HARDWARE.equals("ranchu")) {
return false;
}
if (!Build.BRAND.startsWith("generic") && !Build.BRAND.equals("Android")) {
return false;
}
if (!Build.DEVICE.startsWith("generic")) {
return false;
}
if (!Build.PRODUCT.contains("sdk")) {
return false;
}
if (!Build.MODEL.toLowerCase(Locale.US).contains("sdk")) {
return false;
}
return true;
}
private final FlipGesture mFlipGesture;
private final EmulatorConnector mEmulatorConnector;
}
private class ViewCrawlerHandler extends Handler {
public ViewCrawlerHandler(Context context, String token, Looper looper, ViewVisitor.OnLayoutErrorListener layoutErrorListener) {
super(looper);
mToken = token;
mSnapshot = null;
String resourcePackage = mConfig.getResourcePackageName();
if (null == resourcePackage) {
resourcePackage = context.getPackageName();
}
final ResourceIds resourceIds = new ResourceReader.Ids(resourcePackage, context);
mImageStore = new ImageStore(context, "ViewCrawler");
mProtocol = new EditProtocol(context, resourceIds, mImageStore, layoutErrorListener);
mEditorChanges = new HashMap<String, Pair<String, JSONObject>>();
mEditorTweaks = new ArrayList<JSONObject>();
mEditorAssetUrls = new ArrayList<String>();
mEditorEventBindings = new ArrayList<Pair<String, JSONObject>>();
mPersistentChanges = new ArrayList<VariantChange>();
mPersistentTweaks = new ArrayList<VariantTweak>();
mPersistentEventBindings = new ArrayList<Pair<String, JSONObject>>();
mSeenExperiments = new HashSet<Pair<Integer, Integer>>();
mStartLock = new ReentrantLock();
mStartLock.lock();
}
public void start() {
mStartLock.unlock();
}
@Override
public void handleMessage(Message msg) {
mStartLock.lock();
try {
final int what = msg.what;
switch (what) {
case MESSAGE_INITIALIZE_CHANGES:
loadKnownChanges();
initializeChanges();
break;
case MESSAGE_CONNECT_TO_EDITOR:
connectToEditor();
break;
case MESSAGE_SEND_DEVICE_INFO:
sendDeviceInfo();
break;
case MESSAGE_SEND_STATE_FOR_EDITING:
sendSnapshot((JSONObject) msg.obj);
break;
case MESSAGE_SEND_EVENT_TRACKED:
sendReportTrackToEditor((String) msg.obj);
break;
case MESSAGE_SEND_LAYOUT_ERROR:
sendLayoutError((ViewVisitor.LayoutErrorMessage) msg.obj);
break;
case MESSAGE_VARIANTS_RECEIVED:
handleVariantsReceived((JSONArray) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED:
handleEditorChangeReceived((JSONObject) msg.obj);
break;
case MESSAGE_EVENT_BINDINGS_RECEIVED:
handleEventBindingsReceived((JSONArray) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED:
handleEditorBindingsReceived((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED:
handleEditorBindingsCleared((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_TWEAKS_RECEIVED:
handleEditorTweaksReceived((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CLOSED:
handleEditorClosed();
break;
}
} finally {
mStartLock.unlock();
}
}
/**
* Load the experiment ids and variants already in persistent storage into
* into our set of seen experiments, so we don't double track them.
*/
private void loadKnownChanges() {
final SharedPreferences preferences = getSharedPreferences();
final String storedChanges = preferences.getString(SHARED_PREF_CHANGES_KEY, null);
if (null != storedChanges) {
try {
final JSONArray variants = new JSONArray(storedChanges);
final int variantsLength = variants.length();
for (int i = 0; i < variantsLength; i++) {
final JSONObject variant = variants.getJSONObject(i);
final int variantId = variant.getInt("id");
final int experimentId = variant.getInt("experiment_id");
final Pair<Integer,Integer> sight = new Pair<Integer,Integer>(experimentId, variantId);
mSeenExperiments.add(sight);
}
} catch (JSONException e) {
MPLog.e(LOGTAG, "Malformed variants found in persistent storage, clearing all variants", e);
final SharedPreferences.Editor editor = preferences.edit();
editor.remove(SHARED_PREF_CHANGES_KEY);
editor.remove(SHARED_PREF_BINDINGS_KEY);
editor.apply();
}
}
}
/**
* Load stored changes from persistent storage and apply them to the application.
*/
private void initializeChanges() {
final SharedPreferences preferences = getSharedPreferences();
final String storedChanges = preferences.getString(SHARED_PREF_CHANGES_KEY, null);
final String storedBindings = preferences.getString(SHARED_PREF_BINDINGS_KEY, null);
List<Pair<Integer, Integer>> emptyVariantIds = new ArrayList<>();
try {
mPersistentChanges.clear();
mPersistentTweaks.clear();
if (null != storedChanges) {
final JSONArray variants = new JSONArray(storedChanges);
final int variantsLength = variants.length();
for (int variantIx = 0; variantIx < variantsLength; variantIx++) {
final JSONObject nextVariant = variants.getJSONObject(variantIx);
final int variantIdPart = nextVariant.getInt("id");
final int experimentIdPart = nextVariant.getInt("experiment_id");
final Pair<Integer, Integer> variantId = new Pair<Integer, Integer>(experimentIdPart, variantIdPart);
final JSONArray actions = nextVariant.getJSONArray("actions");
final int actionsLength = actions.length();
for (int i = 0; i < actionsLength; i++) {
final JSONObject change = actions.getJSONObject(i);
final String targetActivity = JSONUtils.optionalStringKey(change, "target_activity");
final VariantChange variantChange = new VariantChange(targetActivity, change, variantId);
mPersistentChanges.add(variantChange);
}
final JSONArray tweaks = nextVariant.getJSONArray("tweaks");
final int tweaksLength = tweaks.length();
for (int i = 0; i < tweaksLength; i++) {
final JSONObject tweakDesc = tweaks.getJSONObject(i);
final VariantTweak variantTweak = new VariantTweak(tweakDesc, variantId);
mPersistentTweaks.add(variantTweak);
}
if (actionsLength == 0 && tweaksLength == 0) {
final Pair<Integer, Integer> emptyVariantId = new Pair<Integer, Integer>(experimentIdPart, variantIdPart);
emptyVariantIds.add(emptyVariantId);
}
}
}
if (null != storedBindings) {
final JSONArray bindings = new JSONArray(storedBindings);
mPersistentEventBindings.clear();
for (int i = 0; i < bindings.length(); i++) {
final JSONObject event = bindings.getJSONObject(i);
final String targetActivity = JSONUtils.optionalStringKey(event, "target_activity");
mPersistentEventBindings.add(new Pair<String, JSONObject>(targetActivity, event));
}
}
} catch (final JSONException e) {
MPLog.i(LOGTAG, "JSON error when initializing saved changes, clearing persistent memory", e);
final SharedPreferences.Editor editor = preferences.edit();
editor.remove(SHARED_PREF_CHANGES_KEY);
editor.remove(SHARED_PREF_BINDINGS_KEY);
editor.apply();
}
applyVariantsAndEventBindings(emptyVariantIds);
}
/**
* Try to connect to the remote interactive editor, if a connection does not already exist.
*/
private void connectToEditor() {
MPLog.v(LOGTAG, "connecting to editor");
if (mEditorConnection != null && mEditorConnection.isValid()) {
MPLog.v(LOGTAG, "There is already a valid connection to an events editor.");
return;
}
final SSLSocketFactory socketFactory = mConfig.getSSLSocketFactory();
if (null == socketFactory) {
MPLog.v(LOGTAG, "SSL is not available on this device, no connection will be attempted to the events editor.");
return;
}
final String url = MPConfig.getInstance(mContext).getEditorUrl() + mToken;
try {
final Socket sslSocket = socketFactory.createSocket();
mEditorConnection = new EditorConnection(new URI(url), new Editor(), sslSocket);
} catch (final URISyntaxException e) {
MPLog.e(LOGTAG, "Error parsing URI " + url + " for editor websocket", e);
} catch (final EditorConnection.EditorConnectionException e) {
MPLog.e(LOGTAG, "Error connecting to URI " + url, e);
} catch (final IOException e) {
MPLog.i(LOGTAG, "Can't create SSL Socket to connect to editor service", e);
}
}
/**
* Send a string error message to the connected web UI.
*/
private void sendError(String errorMessage) {
if (mEditorConnection == null || !mEditorConnection.isValid()) {
return;
}
final JSONObject errorObject = new JSONObject();
try {
errorObject.put("error_message", errorMessage);
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Apparently impossible JSONException", e);
}
final OutputStreamWriter writer = new OutputStreamWriter(mEditorConnection.getBufferedOutputStream());
try {
writer.write("{\"type\": \"error\", ");
writer.write("\"payload\": ");
writer.write(errorObject.toString());
writer.write("}");
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write error message to editor", e);
} finally {
try {
writer.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Could not close output writer to editor", e);
}
}
}
/**
* Report on device info to the connected web UI.
*/
private void sendDeviceInfo() {
if (mEditorConnection == null || !mEditorConnection.isValid()) {
return;
}
final OutputStream out = mEditorConnection.getBufferedOutputStream();
final JsonWriter j = new JsonWriter(new OutputStreamWriter(out));
try {
j.beginObject();
j.name("type").value("device_info_response");
j.name("payload").beginObject();
j.name("device_type").value("Android");
j.name("device_name").value(Build.BRAND + "/" + Build.MODEL);
j.name("scaled_density").value(mScaledDensity);
for (final Map.Entry<String, String> entry : mDeviceInfo.entrySet()) {
j.name(entry.getKey()).value(entry.getValue());
}
final Map<String, Tweaks.TweakValue> tweakDescs = mTweaks.getAllValues();
j.name("tweaks").beginArray();
for (Map.Entry<String, Tweaks.TweakValue> tweak:tweakDescs.entrySet()) {
final Tweaks.TweakValue desc = tweak.getValue();
final String tweakName = tweak.getKey();
j.beginObject();
j.name("name").value(tweakName);
j.name("minimum").value((Number) null);
j.name("maximum").value((Number) null);
switch (desc.type) {
case Tweaks.BOOLEAN_TYPE:
j.name("type").value("boolean");
j.name("value").value(desc.getBooleanValue());
break;
case Tweaks.DOUBLE_TYPE:
j.name("type").value("number");
j.name("encoding").value("d");
j.name("value").value(desc.getNumberValue().doubleValue());
break;
case Tweaks.LONG_TYPE:
j.name("type").value("number");
j.name("encoding").value("l");
j.name("value").value(desc.getNumberValue().longValue());
break;
case Tweaks.STRING_TYPE:
j.name("type").value("string");
j.name("value").value(desc.getStringValue());
break;
default:
MPLog.wtf(LOGTAG, "Unrecognized Tweak Type " + desc.type + " encountered.");
}
j.endObject();
}
j.endArray();
j.endObject(); // payload
j.endObject();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write device_info to server", e);
} finally {
try {
j.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't close websocket writer", e);
}
}
}
/**
* Send a snapshot response, with crawled views and screenshot image, to the connected web UI.
*/
private void sendSnapshot(JSONObject message) {
final long startSnapshot = System.currentTimeMillis();
try {
final JSONObject payload = message.getJSONObject("payload");
if (payload.has("config")) {
mSnapshot = mProtocol.readSnapshotConfig(payload);
MPLog.v(LOGTAG, "Initializing snapshot with configuration");
}
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Payload with snapshot config required with snapshot request", e);
sendError("Payload with snapshot config required with snapshot request");
return;
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Editor sent malformed message with snapshot request", e);
sendError(e.getMessage());
return;
}
if (null == mSnapshot) {
sendError("No snapshot configuration (or a malformed snapshot configuration) was sent.");
MPLog.w(LOGTAG, "Mixpanel editor is misconfigured, sent a snapshot request without a valid configuration.");
return;
}
// ELSE config is valid:
final OutputStream out = mEditorConnection.getBufferedOutputStream();
final OutputStreamWriter writer = new OutputStreamWriter(out);
try {
writer.write("{");
writer.write("\"type\": \"snapshot_response\",");
writer.write("\"payload\": {");
{
writer.write("\"activities\":");
writer.flush();
mSnapshot.snapshots(mEditState, out);
}
final long snapshotTime = System.currentTimeMillis() - startSnapshot;
writer.write(",\"snapshot_time_millis\": ");
writer.write(Long.toString(snapshotTime));
writer.write("}"); // } payload
writer.write("}"); // } whole message
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write snapshot request to server", e);
} finally {
try {
writer.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't close writer.", e);
}
}
}
/**
* Report that a track has occurred to the connected web UI.
*/
private void sendReportTrackToEditor(String eventName) {
if (mEditorConnection == null || !mEditorConnection.isValid()) {
return;
}
final OutputStream out = mEditorConnection.getBufferedOutputStream();
final OutputStreamWriter writer = new OutputStreamWriter(out);
final JsonWriter j = new JsonWriter(writer);
try {
j.beginObject();
j.name("type").value("track_message");
j.name("payload");
{
j.beginObject();
j.name("event_name").value(eventName);
j.endObject();
}
j.endObject();
j.flush();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write track_message to server", e);
} finally {
try {
j.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't close writer.", e);
}
}
}
private void sendLayoutError(ViewVisitor.LayoutErrorMessage exception) {
if (mEditorConnection == null ) {
return;
}
final OutputStream out = mEditorConnection.getBufferedOutputStream();
final OutputStreamWriter writer = new OutputStreamWriter(out);
final JsonWriter j = new JsonWriter(writer);
try {
j.beginObject();
j.name("type").value("layout_error");
j.name("exception_type").value(exception.getErrorType());
j.name("cid").value(exception.getName());
j.endObject();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write track_message to server", e);
} finally {
try {
j.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't close writer.", e);
}
}
}
/**
* Accept and apply a change from the connected UI.
*/
private void handleEditorChangeReceived(JSONObject changeMessage) {
try {
final JSONObject payload = changeMessage.getJSONObject("payload");
final JSONArray actions = payload.getJSONArray("actions");
for (int i = 0; i < actions.length(); i++) {
final JSONObject change = actions.getJSONObject(i);
final String targetActivity = JSONUtils.optionalStringKey(change, "target_activity");
final String name = change.getString("name");
mEditorChanges.put(name, new Pair<String, JSONObject>(targetActivity, change));
}
applyVariantsAndEventBindings(Collections.<Pair<Integer, Integer>>emptyList());
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad change request received", e);
}
}
/**
* Remove a change from the connected UI.
*/
private void handleEditorBindingsCleared(JSONObject clearMessage) {
try {
final JSONObject payload = clearMessage.getJSONObject("payload");
final JSONArray actions = payload.getJSONArray("actions");
// Don't throw any JSONExceptions after this, or you'll leak the item
for (int i = 0; i < actions.length(); i++) {
final String changeId = actions.getString(i);
mEditorChanges.remove(changeId);
}
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad clear request received", e);
}
applyVariantsAndEventBindings(Collections.<Pair<Integer, Integer>>emptyList());
}
private void handleEditorTweaksReceived(JSONObject tweaksMessage) {
try {
mEditorTweaks.clear();
final JSONObject payload = tweaksMessage.getJSONObject("payload");
final JSONArray tweaks = payload.getJSONArray("tweaks");
final int length = tweaks.length();
for (int i = 0; i < length; i++) {
final JSONObject tweakDesc = tweaks.getJSONObject(i);
mEditorTweaks.add(tweakDesc);
}
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad tweaks received", e);
}
applyVariantsAndEventBindings(Collections.<Pair<Integer, Integer>>emptyList());
}
/**
* Accept and apply variant changes from a non-interactive source.
*/
private void handleVariantsReceived(JSONArray variants) {
final SharedPreferences preferences = getSharedPreferences();
final SharedPreferences.Editor editor = preferences.edit();
if(variants.length() > 0) {
editor.putString(SHARED_PREF_CHANGES_KEY, variants.toString());
} else {
editor.remove(SHARED_PREF_CHANGES_KEY);
}
editor.apply();
initializeChanges();
}
/**
* Accept and apply a persistent event binding from a non-interactive source.
*/
private void handleEventBindingsReceived(JSONArray eventBindings) {
final SharedPreferences preferences = getSharedPreferences();
final SharedPreferences.Editor editor = preferences.edit();
editor.putString(SHARED_PREF_BINDINGS_KEY, eventBindings.toString());
editor.apply();
initializeChanges();
}
/**
* Accept and apply a temporary event binding from the connected UI.
*/
private void handleEditorBindingsReceived(JSONObject message) {
final JSONArray eventBindings;
try {
final JSONObject payload = message.getJSONObject("payload");
eventBindings = payload.getJSONArray("events");
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad event bindings received", e);
return;
}
final int eventCount = eventBindings.length();
mEditorEventBindings.clear();
for (int i = 0; i < eventCount; i++) {
try {
final JSONObject event = eventBindings.getJSONObject(i);
final String targetActivity = JSONUtils.optionalStringKey(event, "target_activity");
mEditorEventBindings.add(new Pair<String, JSONObject>(targetActivity, event));
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad event binding received from editor in " + eventBindings.toString(), e);
}
}
applyVariantsAndEventBindings(Collections.<Pair<Integer, Integer>>emptyList());
}
/**
* Clear state associated with the editor now that the editor is gone.
*/
private void handleEditorClosed() {
mEditorChanges.clear();
mEditorEventBindings.clear();
// Free (or make available) snapshot memory
mSnapshot = null;
MPLog.v(LOGTAG, "Editor closed- freeing snapshot");
applyVariantsAndEventBindings(Collections.<Pair<Integer, Integer>>emptyList());
for (final String assetUrl:mEditorAssetUrls) {
mImageStore.deleteStorage(assetUrl);
}
}
/**
* Reads our JSON-stored edits from memory and submits them to our EditState. Overwrites
* any existing edits at the time that it is run.
*
* applyVariantsAndEventBindings should be called any time we load new edits, event bindings,
* or tweaks from disk or when we receive new edits from the interactive UI editor.
* Changes and event bindings from our persistent storage and temporary changes
* received from interactive editing will all be submitted to our EditState, tweaks
* will be updated, and experiment statuses will be tracked.
*/
private void applyVariantsAndEventBindings(List<Pair<Integer, Integer>> emptyVariantIds) {
final List<Pair<String, ViewVisitor>> newVisitors = new ArrayList<Pair<String, ViewVisitor>>();
final Set<Pair<Integer, Integer>> toTrack = new HashSet<Pair<Integer, Integer>>();
{
final int size = mPersistentChanges.size();
for (int i = 0; i < size; i++) {
final VariantChange changeInfo = mPersistentChanges.get(i);
try {
final EditProtocol.Edit edit = mProtocol.readEdit(changeInfo.change);
newVisitors.add(new Pair<String, ViewVisitor>(changeInfo.activityName, edit.visitor));
if (!mSeenExperiments.contains(changeInfo.variantId)) {
toTrack.add(changeInfo.variantId);
}
} catch (final EditProtocol.CantGetEditAssetsException e) {
MPLog.v(LOGTAG, "Can't load assets for an edit, won't apply the change now", e);
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad persistent change request cannot be applied.", e);
}
}
}
{
boolean isTweaksUpdated = false;
final int size = mPersistentTweaks.size();
for (int i = 0; i < size; i++) {
final VariantTweak tweakInfo = mPersistentTweaks.get(i);
try {
final Pair<String, Object> tweakValue = mProtocol.readTweak(tweakInfo.tweak);
if (!mSeenExperiments.contains(tweakInfo.variantId)) {
toTrack.add(tweakInfo.variantId);
isTweaksUpdated = true;
} else if (mTweaks.isNewValue(tweakValue.first, tweakValue.second)) {
isTweaksUpdated = true;
}
mTweaks.set(tweakValue.first, tweakValue.second);
} catch (EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad editor tweak cannot be applied.", e);
}
}
if (isTweaksUpdated) {
for (OnMixpanelTweaksUpdatedListener listener : mTweaksUpdatedListeners) {
listener.onMixpanelTweakUpdated();
}
}
if(size == 0) { // there are no new tweaks, so reset to default values
final Map<String, Tweaks.TweakValue> tweakDefaults = mTweaks.getDefaultValues();
for (Map.Entry<String, Tweaks.TweakValue> tweak:tweakDefaults.entrySet()) {
final Tweaks.TweakValue tweakValue = tweak.getValue();
final String tweakName = tweak.getKey();
mTweaks.set(tweakName, tweakValue);
}
}
}
{
for (Pair<String, JSONObject> changeInfo:mEditorChanges.values()) {
try {
final EditProtocol.Edit edit = mProtocol.readEdit(changeInfo.second);
newVisitors.add(new Pair<String, ViewVisitor>(changeInfo.first, edit.visitor));
mEditorAssetUrls.addAll(edit.imageUrls);
} catch (final EditProtocol.CantGetEditAssetsException e) {
MPLog.v(LOGTAG, "Can't load assets for an edit, won't apply the change now", e);
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad editor change request cannot be applied.", e);
}
}
}
{
final int size = mEditorTweaks.size();
for (int i = 0; i < size; i++) {
final JSONObject tweakDesc = mEditorTweaks.get(i);
try {
final Pair<String, Object> tweakValue = mProtocol.readTweak(tweakDesc);
mTweaks.set(tweakValue.first, tweakValue.second);
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Strange tweaks received", e);
}
}
}
{
final int size = mPersistentEventBindings.size();
for (int i = 0; i < size; i++) {
final Pair<String, JSONObject> changeInfo = mPersistentEventBindings.get(i);
try {
final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
newVisitors.add(new Pair<String, ViewVisitor>(changeInfo.first, visitor));
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad persistent event binding cannot be applied.", e);
}
}
}
{
final int size = mEditorEventBindings.size();
for (int i = 0; i < size; i++) {
final Pair<String, JSONObject> changeInfo = mEditorEventBindings.get(i);
try {
final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
newVisitors.add(new Pair<String, ViewVisitor>(changeInfo.first, visitor));
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad editor event binding cannot be applied.", e);
}
}
}
final Map<String, List<ViewVisitor>> editMap = new HashMap<String, List<ViewVisitor>>();
final int totalEdits = newVisitors.size();
for (int i = 0; i < totalEdits; i++) {
final Pair<String, ViewVisitor> next = newVisitors.get(i);
final List<ViewVisitor> mapElement;
if (editMap.containsKey(next.first)) {
mapElement = editMap.get(next.first);
} else {
mapElement = new ArrayList<ViewVisitor>();
editMap.put(next.first, mapElement);
}
mapElement.add(next.second);
}
mEditState.setEdits(editMap);
for (Pair<Integer, Integer> id : emptyVariantIds) {
if (!mSeenExperiments.contains(id)) {
toTrack.add(id);
}
}
mSeenExperiments.addAll(toTrack);
if (toTrack.size() > 0) {
final JSONObject variantObject = new JSONObject();
try {
for (Pair<Integer, Integer> variant : toTrack) {
final int experimentId = variant.first;
final int variantId = variant.second;
final JSONObject trackProps = new JSONObject();
trackProps.put("$experiment_id", experimentId);
trackProps.put("$variant_id", variantId);
variantObject.put(Integer.toString(experimentId), variantId);
mMixpanel.getPeople().merge("$experiments", variantObject);
mMixpanel.updateSuperProperties(new SuperPropertyUpdate() {
public JSONObject update(JSONObject in) {
try {
in.put("$experiments", variantObject);
} catch (JSONException e) {
MPLog.wtf(LOGTAG, "Can't write $experiments super property", e);
}
return in;
}
});
mMixpanel.track("$experiment_started", trackProps);
}
} catch (JSONException e) {
MPLog.wtf(LOGTAG, "Could not build JSON for reporting experiment start", e);
}
}
}
private SharedPreferences getSharedPreferences() {
final String sharedPrefsName = SHARED_PREF_EDITS_FILE + mToken;
return mContext.getSharedPreferences(sharedPrefsName, Context.MODE_PRIVATE);
}
private EditorConnection mEditorConnection;
private ViewSnapshot mSnapshot;
private final String mToken;
private final Lock mStartLock;
private final EditProtocol mProtocol;
private final ImageStore mImageStore;
private final Map<String, Pair<String,JSONObject>> mEditorChanges;
private final List<JSONObject> mEditorTweaks;
private final List<String> mEditorAssetUrls;
private final List<Pair<String,JSONObject>> mEditorEventBindings;
private final List<VariantChange> mPersistentChanges;
private final List<VariantTweak> mPersistentTweaks;
private final List<Pair<String,JSONObject>> mPersistentEventBindings;
private final Set<Pair<Integer, Integer>> mSeenExperiments;
}
private class Editor implements EditorConnection.Editor {
@Override
public void sendSnapshot(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_STATE_FOR_EDITING);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void performEdit(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void clearEdits(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void setTweaks(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_HANDLE_EDITOR_TWEAKS_RECEIVED);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void bindEvents(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void sendDeviceInfo() {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_DEVICE_INFO);
mMessageThreadHandler.sendMessage(msg);
}
@Override
public void cleanup() {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_HANDLE_EDITOR_CLOSED);
mMessageThreadHandler.sendMessage(msg);
}
}
private static class VariantChange {
public VariantChange(String anActivityName, JSONObject someChange, Pair<Integer, Integer> aVariantId) {
activityName = anActivityName;
change = someChange;
variantId = aVariantId;
}
public final String activityName;
public final JSONObject change;
public final Pair<Integer, Integer> variantId;
}
private static class VariantTweak {
public VariantTweak(JSONObject aTweak, Pair<Integer, Integer> aVariantId) {
tweak = aTweak;
variantId = aVariantId;
}
public final JSONObject tweak;
public final Pair<Integer, Integer> variantId;
}
private final MPConfig mConfig;
private final Context mContext;
private final MixpanelAPI mMixpanel;
private final DynamicEventTracker mDynamicEventTracker;
private final EditState mEditState;
private final Tweaks mTweaks;
private final Map<String, String> mDeviceInfo;
private final ViewCrawlerHandler mMessageThreadHandler;
private final float mScaledDensity;
private final Set<OnMixpanelTweaksUpdatedListener> mTweaksUpdatedListeners;
private static final String SHARED_PREF_EDITS_FILE = "mixpanel.viewcrawler.changes";
private static final String SHARED_PREF_CHANGES_KEY = "mixpanel.viewcrawler.changes";
private static final String SHARED_PREF_BINDINGS_KEY = "mixpanel.viewcrawler.bindings";
private static final int MESSAGE_INITIALIZE_CHANGES = 0;
private static final int MESSAGE_CONNECT_TO_EDITOR = 1;
private static final int MESSAGE_SEND_STATE_FOR_EDITING = 2;
private static final int MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED = 3;
private static final int MESSAGE_SEND_DEVICE_INFO = 4;
private static final int MESSAGE_EVENT_BINDINGS_RECEIVED = 5;
private static final int MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED = 6;
private static final int MESSAGE_SEND_EVENT_TRACKED = 7;
private static final int MESSAGE_HANDLE_EDITOR_CLOSED = 8;
private static final int MESSAGE_VARIANTS_RECEIVED = 9;
private static final int MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED = 10;
private static final int MESSAGE_HANDLE_EDITOR_TWEAKS_RECEIVED = 11;
private static final int MESSAGE_SEND_LAYOUT_ERROR = 12;
private static final int EMULATOR_CONNECT_ATTEMPT_INTERVAL_MILLIS = 1000 * 30;
@SuppressWarnings("unused")
private static final String LOGTAG = "MixpanelAPI.ViewCrawler";
}