package com.mixpanel.android.viewcrawler; import android.app.Activity; import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.ViewTreeObserver; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Handles applying and managing the life cycle of edits in an application. Clients * can replace all of the edits in an app with {@link EditState#setEdits(java.util.Map)}. * * Some client is responsible for informing the EditState about the presence or absence * of Activities, by calling {@link EditState#add(android.app.Activity)} and {@link EditState#remove(android.app.Activity)} */ /* package */ class EditState extends UIThreadSet<Activity> { public EditState() { mUiThreadHandler = new Handler(Looper.getMainLooper()); mIntendedEdits = new HashMap<String, List<ViewVisitor>>(); mCurrentEdits = new HashSet<EditBinding>(); } /** * Should be called whenever a new Activity appears in the application. */ @Override public void add(Activity newOne) { super.add(newOne); applyEditsOnUiThread(); } /** * Should be called whenever an activity leaves the application, or is otherwise no longer relevant to our edits. */ @Override public void remove(Activity oldOne) { super.remove(oldOne); } /** * Sets the entire set of edits to be applied to the application. * * Edits are represented by ViewVisitors, batched in a map by the String name of the activity * they should be applied to. Edits to apply to all views should be in a list associated with * the key {@code null} (Not the string "null", the actual null value!) * * The given edits will completely replace any existing edits. * * setEdits can be called from any thread, although the changes will occur (eventually) on the * UI thread of the application, and may not appear immediately. * * @param newEdits A Map from activity name to a list of edits to apply */ // Must be thread-safe public void setEdits(Map<String, List<ViewVisitor>> newEdits) { // Delete images that are no longer needed synchronized (mCurrentEdits) { for (final EditBinding stale : mCurrentEdits) { stale.kill(); } mCurrentEdits.clear(); } synchronized(mIntendedEdits) { mIntendedEdits.clear(); mIntendedEdits.putAll(newEdits); } applyEditsOnUiThread(); } private void applyEditsOnUiThread() { if (Thread.currentThread() == mUiThreadHandler.getLooper().getThread()) { applyIntendedEdits(); } else { mUiThreadHandler.post(new Runnable() { @Override public void run() { applyIntendedEdits(); } }); } } // Must be called on UI Thread private void applyIntendedEdits() { for (final Activity activity : getAll()) { final String activityName = activity.getClass().getCanonicalName(); final View rootView = activity.getWindow().getDecorView().getRootView(); final List<ViewVisitor> specificChanges; final List<ViewVisitor> wildcardChanges; synchronized (mIntendedEdits) { specificChanges = mIntendedEdits.get(activityName); wildcardChanges = mIntendedEdits.get(null); } if (null != specificChanges) { applyChangesFromList(rootView, specificChanges); } if (null != wildcardChanges) { applyChangesFromList(rootView, wildcardChanges); } } } // Must be called on UI Thread private void applyChangesFromList(View rootView, List<ViewVisitor> changes) { synchronized (mCurrentEdits) { final int size = changes.size(); for (int i = 0; i < size; i++) { final ViewVisitor visitor = changes.get(i); final EditBinding binding = new EditBinding(rootView, visitor, mUiThreadHandler); mCurrentEdits.add(binding); } } } /* The binding between a bunch of edits and a view. Should be instantiated and live on the UI thread */ private static class EditBinding implements ViewTreeObserver.OnGlobalLayoutListener, Runnable { public EditBinding(View viewRoot, ViewVisitor edit, Handler uiThreadHandler) { mEdit = edit; mViewRoot = new WeakReference<View>(viewRoot); mHandler = uiThreadHandler; mAlive = true; mDying = false; final ViewTreeObserver observer = viewRoot.getViewTreeObserver(); if (observer.isAlive()) { observer.addOnGlobalLayoutListener(this); } run(); } @Override public void onGlobalLayout() { run(); } @Override public void run() { if (!mAlive) { return; } final View viewRoot = mViewRoot.get(); if (null == viewRoot || mDying) { cleanUp(); return; } // ELSE View is alive and we are alive mEdit.visit(viewRoot); mHandler.removeCallbacks(this); mHandler.postDelayed(this, 1000); } public void kill() { mDying = true; mHandler.post(this); } @SuppressWarnings("deprecation") private void cleanUp() { if (mAlive) { final View viewRoot = mViewRoot.get(); if (null != viewRoot) { final ViewTreeObserver observer = viewRoot.getViewTreeObserver(); if (observer.isAlive()) { observer.removeGlobalOnLayoutListener(this); // Deprecated Name } } mEdit.cleanup(); } mAlive = false; } private volatile boolean mDying; private boolean mAlive; private final WeakReference<View> mViewRoot; private final ViewVisitor mEdit; private final Handler mHandler; } private final Handler mUiThreadHandler; private final Map<String, List<ViewVisitor>> mIntendedEdits; private final Set<EditBinding> mCurrentEdits; @SuppressWarnings("unused") private static final String LOGTAG = "MixpanelAPI.EditState"; }