Bug 1361664 - Part2. Create MVP Triggers and Deeplinks for Mobile Marketing Automatin. r?maliu draft
authorNevin Chen <cnevinchen@gmail.com>
Wed, 17 May 2017 01:25:45 +0800
changeset 586219 ffe4f8315ec57f27951778f1a819c2a7911cec21
parent 586218 e737c1995f93a1fef688d035614844f7ef6b62ba
child 630923 92d10fbf321b271b0b06b57b07711367c8437aee
push id61332
push userbmo:cnevinchen@gmail.com
push dateTue, 30 May 2017 00:29:11 +0000
reviewersmaliu
bugs1361664
milestone55.0a1
Bug 1361664 - Part2. Create MVP Triggers and Deeplinks for Mobile Marketing Automatin. r?maliu This patch do 6 things. They are related so I put them in the same patch. 1. Extract MmaEvent Name 2. If MMA is diabled, don't send event. 3. Add check before sending 'Set Default Borwser' deep link 4. Add user attribute for delay messaging focus install status. 5. If the user pref off at runtime, we ask Leanplum to stop and prevent our app sending event to Leanplum. 6. Fix proguard. Only keep necceary files. MozReview-Commit-ID: APEmr1JXBLH
mobile/android/base/generate_build_config.py
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/Telemetry.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
mobile/android/chrome/content/Reader.js
mobile/android/chrome/content/browser.js
mobile/android/config/proguard/proguard-leanplum.cfg
--- a/mobile/android/base/generate_build_config.py
+++ b/mobile/android/base/generate_build_config.py
@@ -101,16 +101,17 @@ def _defines():
 
     # It's okay to use MOZ_ADJUST_SDK_KEY here because this doesn't
     # leak the value to build logs.
     if CONFIG['MOZ_INSTALL_TRACKING']:
         DEFINES['MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN'] = CONFIG['MOZ_ADJUST_SDK_KEY']
 
     if CONFIG['MOZ_ANDROID_MMA']:
         DEFINES['MOZ_LEANPLUM_SDK_KEY'] = CONFIG['MOZ_LEANPLUM_SDK_KEY']
+        DEFINES['MOZ_LEANPLUM_SDK_CLIENTID'] = CONFIG['MOZ_LEANPLUM_SDK_CLIENTID']
 
     DEFINES['MOZ_BUILDID'] = open(os.path.join(buildconfig.topobjdir, 'buildid.h')).readline().split()[2]
 
     # Set the appropriate version code if not set by MOZ_APP_ANDROID_VERSION_CODE.
     if CONFIG.get('MOZ_APP_ANDROID_VERSION_CODE'):
         DEFINES['ANDROID_VERSION_CODE'] = \
             CONFIG.get('MOZ_APP_ANDROID_VERSION_CODE')
     else:
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -15,16 +15,17 @@ import org.mozilla.gecko.gfx.FullScreenS
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.health.HealthRecorder;
 import org.mozilla.gecko.health.SessionInformation;
 import org.mozilla.gecko.health.StubbedHealthRecorder;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuInflater;
 import org.mozilla.gecko.menu.MenuPanel;
+import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.PromptService;
@@ -116,16 +117,18 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import static org.mozilla.gecko.Tabs.INTENT_EXTRA_SESSION_UUID;
 import static org.mozilla.gecko.Tabs.INTENT_EXTRA_TAB_ID;
 import static org.mozilla.gecko.Tabs.INVALID_TAB_ID;
+import static org.mozilla.gecko.mma.MmaDelegate.DOWNLOAD_VIDEOS_OR_ANY_OTHER_MEDIA;
+import static org.mozilla.gecko.mma.MmaDelegate.LOADS_ARTICLES;
 
 public abstract class GeckoApp extends GeckoActivity
                                implements AnchoredPopup.OnVisibilityChangeListener,
                                           BundleEventListener,
                                           ContextGetter,
                                           GeckoMenu.Callback,
                                           GeckoMenu.MenuPresenter,
                                           GeckoView.ContentListener,
@@ -865,17 +868,25 @@ public abstract class GeckoApp extends G
 
         } else if ("Update:Install".equals(event)) {
             UpdateServiceHelper.applyUpdate(this);
 
         } else if ("PluginHelper:playFlash".equals(event)) {
             final SharedPreferences prefs = getSharedPreferences();
             int count = prefs.getInt(PREFS_FLASH_USAGE, 0);
             prefs.edit().putInt(PREFS_FLASH_USAGE, ++count).apply();
+
+        } else if ("Mma:reader_available".equals(event)) {
+            MmaDelegate.track(LOADS_ARTICLES);
+
+        } else if ("Mma:web_save_media".equals(event) || "Mma:web_save_image".equals(event)) {
+            MmaDelegate.track(DOWNLOAD_VIDEOS_OR_ANY_OTHER_MEDIA);
+
         }
+
     }
 
     /**
      * To get a presenter which will response for text-selection. In preMarshmallow Android we want
      * to provide different UI action when user select a text. Text-selection class will uses this
      * presenter to trigger UI updating.
      *
      * @return a presenter which handle showing/hiding of action mode UI. return *null* if this
@@ -1391,16 +1402,19 @@ public abstract class GeckoApp extends G
             "Image:SetAs",
             null);
 
         getAppEventDispatcher().registerUiThreadListener(this,
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
+            "Mma:reader_available",
+            "Mma:web_save_image",
+            "Mma:web_save_media",
             "Permissions:Data",
             "PrivateBrowsing:Data",
             "RuntimePermissions:Check",
             "Share:Text",
             "SystemUI:Visibility",
             "ToggleChrome:Focus",
             "ToggleChrome:Hide",
             "ToggleChrome:Show",
@@ -2439,16 +2453,19 @@ public abstract class GeckoApp extends G
             "Image:SetAs",
             null);
 
         getAppEventDispatcher().unregisterUiThreadListener(this,
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
+            "Mma:reader_available",
+            "Mma:web_save_image",
+            "Mma:web_save_media",
             "Permissions:Data",
             "PrivateBrowsing:Data",
             "RuntimePermissions:Check",
             "Share:Text",
             "SystemUI:Visibility",
             "ToggleChrome:Focus",
             "ToggleChrome:Hide",
             "ToggleChrome:Show",
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -16,19 +16,21 @@ import android.content.SharedPreferences
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.distribution.PartnerBrowserCustomizationsClient;
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.WhatsNewReceiver;
 import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.JavaUtil;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.webapps.WebAppActivity;
 
@@ -44,16 +46,17 @@ import android.net.Uri;
 import android.os.Handler;
 import android.provider.Browser;
 import android.support.annotation.UiThread;
 import android.support.v4.content.ContextCompat;
 import android.text.TextUtils;
 import android.util.Log;
 
 import static org.mozilla.gecko.Tab.TabType;
+import static org.mozilla.gecko.mma.MmaDelegate.VISITING_A_WEBSITE_WITH_MATCH_TO_PAST_HISTORY;
 
 public class Tabs implements BundleEventListener {
     private static final String LOGTAG = "GeckoTabs";
 
     public static final String INTENT_EXTRA_TAB_ID = "TabId";
     public static final String INTENT_EXTRA_SESSION_UUID = "SessionUUID";
     private static final String PRIVATE_TAB_INTENT_EXTRA = "private_tab";
 
@@ -1083,26 +1086,35 @@ public class Tabs implements BundleEvent
         EventDispatcher.getInstance().dispatch("Tab:Load", data);
 
         if (tabToSelect == null) {
             return null;
         }
 
         if (!delayLoad && !background) {
             selectTab(tabToSelect.getId());
+            tracking(url);
         }
 
         // Load favicon instantly for about:home page because it's already cached
         if (AboutPages.isBuiltinIconPage(url)) {
             tabToSelect.loadFavicon();
         }
 
+
         return tabToSelect;
     }
 
+    private void tracking(String url) {
+        AddToHomeScreenPromotion.URLHistory history = AddToHomeScreenPromotion.getHistoryForURL(mAppContext, url);
+        if (history != null && history.visits > 0) {
+            MmaDelegate.track(VISITING_A_WEBSITE_WITH_MATCH_TO_PAST_HISTORY, history.visits);
+        }
+    }
+
     /**
      * Opens a new tab and loads either about:home or, if PREFS_HOMEPAGE_FOR_EVERY_NEW_TAB is set,
      * the user's homepage.
      */
     public Tab addTab() {
         return loadUrl(getHomepageForNewTab(mAppContext), Tabs.LOADURL_NEW_TAB);
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
@@ -6,20 +6,28 @@
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.TelemetryContract.Event;
 import org.mozilla.gecko.TelemetryContract.Method;
 import org.mozilla.gecko.TelemetryContract.Reason;
 import org.mozilla.gecko.TelemetryContract.Session;
+import org.mozilla.gecko.mma.MmaDelegate;
 
 import android.os.SystemClock;
 import android.util.Log;
 
+import static org.mozilla.gecko.mma.MmaDelegate.INTERACT_WITH_SEARCH_URL_AREA;
+import static org.mozilla.gecko.mma.MmaDelegate.LOAD_BOOKMARK;
+import static org.mozilla.gecko.mma.MmaDelegate.SAVE_BOOKMARK;
+import static org.mozilla.gecko.mma.MmaDelegate.SAVE_PASSWORD;
+import static org.mozilla.gecko.mma.MmaDelegate.WHEN_USER_TAKE_A_SCREENSHOT;
+
+
 /**
  * All telemetry times are relative to one of two clocks:
  *
  * * Real time since the device was booted, including deep sleep. Use this
  *   as a substitute for wall clock.
  * * Uptime since the device was booted, excluding deep sleep. Use this to
  *   avoid timing a user activity when their phone is in their pocket!
  *
@@ -203,16 +211,34 @@ public class Telemetry {
         }
         if (GeckoThread.isRunning()) {
             nativeAddUiEvent(eventName, method.toString(), timestamp, extras);
         } else {
             GeckoThread.queueNativeCall(Telemetry.class, "nativeAddUiEvent",
                                         String.class, eventName, String.class, method.toString(),
                                         timestamp, String.class, extras);
         }
+        mappingMmaTracking(eventName, method, extras);
+    }
+
+    private static void mappingMmaTracking(String eventName, Method method, String extras) {
+        if (eventName == null || method == null || extras == null) {
+            return;
+        }
+        if (eventName.equalsIgnoreCase(Event.SAVE.toString()) && method == Method.MENU && extras.equals("bookmark")) {
+            MmaDelegate.track(SAVE_BOOKMARK);
+        } else if (eventName.equalsIgnoreCase(Event.LOAD_URL.toString()) && method == Method.LIST_ITEM && extras.equals("bookmarks")) {
+            MmaDelegate.track(LOAD_BOOKMARK);
+        } else if (eventName.equalsIgnoreCase(Event.SHOW.toString()) && method == Method.ACTIONBAR && extras.equals("urlbar-url")) {
+            MmaDelegate.track(INTERACT_WITH_SEARCH_URL_AREA);
+        } else if (eventName.equalsIgnoreCase(Event.SHARE.toString()) && method == Method.BUTTON && extras.equals("screenshot")) {
+            MmaDelegate.track(WHEN_USER_TAKE_A_SCREENSHOT);
+        } else if (eventName.equalsIgnoreCase(Event.ACTION.toString()) && method == Method.DOORHANGER && extras.equals("login-positive")) {
+            MmaDelegate.track(SAVE_PASSWORD);
+        }
     }
 
     public static void sendUIEvent(final Event event, final Method method, final long timestamp,
             final String extras) {
         sendUIEvent(event.toString(), method, timestamp, extras);
     }
 
     public static void sendUIEvent(final Event event, final Method method, final long timestamp) {
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -2,63 +2,122 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.mma;
 
 import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.text.TextUtils;
 import android.util.Log;
 
+import org.mozilla.gecko.Experiments;
 import org.mozilla.gecko.MmaConstants;
 import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.switchboard.SwitchBoard;
+
+import java.lang.ref.WeakReference;
 
 
 public class MmaDelegate {
 
+    public static final String LOADS_ARTICLES = "Loads articles";
+    public static final String DOWNLOAD_VIDEOS_OR_ANY_OTHER_MEDIA = "Download videos or any other media";
+    public static final String CLEAR_PRIVATE_DATA = "Clear Private Data";
+    public static final String SAVE_BOOKMARK = "SaveBookmark";
+    public static final String LOAD_BOOKMARK = "LoadBookmark";
+    public static final String INTERACT_WITH_SEARCH_URL_AREA = "Interact with search url area";
+    public static final String WHEN_USER_TAKE_A_SCREENSHOT = "When user take a screenshot";
+    public static final String SAVE_PASSWORD = "SavePassword";
+    public static final String VISITING_A_WEBSITE_WITH_MATCH_TO_PAST_HISTORY = "Visiting a website with match to past history";
+    public static final String LAUNCH_BUT_NOT_DEFAULT_BROWSER = "Launch but not default browser";
+
+
     private static final String TAG = "MmaDelegate";
     private static final String KEY_PREF_BOOLEAN_MMA_ENABLED = "mma.enabled";
     private static final String[] PREFS = { KEY_PREF_BOOLEAN_MMA_ENABLED };
 
 
     private static boolean isGeckoPrefOn = false;
     private static MmaInterface mmaHelper = MmaConstants.getMma();
-
+    private static WeakReference<Context> applicationContext;
 
     public static void init(Activity activity) {
+        applicationContext = new WeakReference<>(activity.getApplicationContext());
         setupPrefHandler(activity);
     }
 
     public static void stop() {
         mmaHelper.stop();
     }
 
     private static void setupPrefHandler(final Activity activity) {
         PrefsHelper.PrefHandler handler = new PrefsHelper.PrefHandlerBase() {
             @Override
             public void prefValue(String pref, boolean value) {
                 if (pref.equals(KEY_PREF_BOOLEAN_MMA_ENABLED)) {
                     Log.d(TAG, "prefValue() called with: pref = [" + pref + "], value = [" + value + "]");
                     if (value) {
                         mmaHelper.init(activity);
+                        if (!isDefaultBrowser(activity)) {
+                            mmaHelper.track(MmaDelegate.LAUNCH_BUT_NOT_DEFAULT_BROWSER);
+                        }
                         isGeckoPrefOn = true;
                     } else {
                         isGeckoPrefOn = false;
                     }
                 }
             }
         };
         PrefsHelper.addObserver(PREFS, handler);
     }
 
+
     public static void track(String event) {
-        if (isGeckoPrefOn) {
+        if (isMmaEnabled()) {
             mmaHelper.track(event);
         }
     }
 
     public static void track(String event, long value) {
-        if (isGeckoPrefOn) {
+        if (isMmaEnabled()) {
             mmaHelper.track(event, value);
         }
     }
+
+    private static boolean isMmaEnabled() {
+        if (applicationContext == null) {
+            return false;
+        }
+
+        final Context context = applicationContext.get();
+        if (context == null) {
+            return false;
+        }
+
+        final boolean healthReport = GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
+        final boolean inExperiment = SwitchBoard.isInExperiment(context, Experiments.LEANPLUM);
+
+        return inExperiment && healthReport && isGeckoPrefOn;
+    }
+
+
+    private static boolean isDefaultBrowser(Context context) {
+        final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.mozilla.org"));
+        final ResolveInfo info = context.getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
+        if (info == null) {
+            // No default is set
+            return false;
+        }
+
+        final String packageName = info.activityInfo.packageName;
+        return (TextUtils.equals(packageName, context.getPackageName()));
+    }
+
+
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaLeanplumImp.java
@@ -2,57 +2,68 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.mma;
 
 import android.app.Activity;
-import android.app.Application;
 import android.content.Context;
+import android.content.pm.PackageManager;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
-import com.leanplum.annotations.Parser;
 
-import org.mozilla.gecko.ActivityHandlerHelper;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.MmaConstants;
-import org.mozilla.gecko.util.ContextUtils;
 
 import java.util.HashMap;
 import java.util.Map;
 
 
 public class MmaLeanplumImp implements MmaInterface {
     @Override
-    public void init(Activity activity) {
+    public void init(final Activity activity) {
+        if (activity == null) {
+            return;
+        }
         Leanplum.setApplicationContext(activity.getApplicationContext());
 
         LeanplumActivityHelper.enableLifecycleCallbacks(activity.getApplication());
 
         if (AppConstants.MOZILLA_OFFICIAL) {
             Leanplum.setAppIdForProductionMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         } else {
             Leanplum.setAppIdForDevelopmentMode(MmaConstants.MOZ_LEANPLUM_SDK_CLIENTID, MmaConstants.MOZ_LEANPLUM_SDK_KEY);
         }
 
-
         Map<String, Object> attributes = new HashMap<>();
-        boolean installedFocus = ContextUtils.isPackageInstalled(activity, "org.mozilla.focus");
-        boolean installedKlar = ContextUtils.isPackageInstalled(activity, "org.mozilla.klar");
+        boolean installedFocus = isPackageInstalled(activity, "org.mozilla.focus");
+        boolean installedKlar = isPackageInstalled(activity, "org.mozilla.klar");
         if (installedFocus || installedKlar) {
             attributes.put("focus", "installed");
         }
         Leanplum.start(activity, attributes);
-        Leanplum.track("Launch");
+
         // this is special to Leanplum. Since we defer LeanplumActivityHelper's onResume call till
-        // switchboard completes loading, we manually call it here.
-        LeanplumActivityHelper.onResume(activity);
+        // switchboard completes loading. We miss the call to LeanplumActivityHelper.onResume.
+        // So I manually call it here.
+        //
+        // There's a risk that if this is called after activity's onPause(Although I've
+        // tested it's seems okay). We  should require their SDK to separate activity call back with
+        // SDK initialization and Activity lifecycle in the future.
+        //
+        // I put it under runOnUiThread because in current Leanplum's SDK design, this should be run in main thread.
+        activity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                LeanplumActivityHelper.onResume(activity);
+            }
+        });
     }
 
     @Override
     public void start(Context context) {
 
     }
 
     @Override
@@ -66,9 +77,19 @@ public class MmaLeanplumImp implements M
         Leanplum.track(leanplumEvent, value);
 
     }
 
     @Override
     public void stop() {
         Leanplum.stop();
     }
+
+    private static boolean isPackageInstalled(final Context context, String packageName) {
+        try {
+            PackageManager pm = context.getPackageManager();
+            pm.getPackageInfo(packageName, 0);
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
@@ -3,28 +3,32 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.preferences;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.util.GeckoBundle;
 
 import org.mozilla.gecko.icons.storage.DiskStorage;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Set;
 
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
 
+import static org.mozilla.gecko.mma.MmaDelegate.CLEAR_PRIVATE_DATA;
+
+
 class PrivateDataPreference extends MultiPrefMultiChoicePreference {
     private static final String LOGTAG = "GeckoPrivateDataPreference";
     private static final String PREF_KEY_PREFIX = "private.data.";
 
     public PrivateDataPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
@@ -53,10 +57,11 @@ class PrivateDataPreference extends Mult
 
         if (values.contains("private.data.offlineApps")) {
             // Remove all icons from storage if removing "Offline website data" was selected.
             DiskStorage.get(getContext()).evictAll();
         }
 
         // clear private data in gecko
         EventDispatcher.getInstance().dispatch("Sanitize:ClearData", data);
+        MmaDelegate.track(CLEAR_PRIVATE_DATA);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -31,17 +31,17 @@ import org.mozilla.gecko.util.ThreadUtil
 import java.lang.ref.WeakReference;
 
 import ch.boye.httpclientandroidlib.util.TextUtils;
 
 /**
  * Promote "Add to home screen" if user visits website often.
  */
 public class AddToHomeScreenPromotion extends TabsTrayVisibilityAwareDelegate implements Tabs.OnTabsChangedListener {
-    private static class URLHistory {
+    public static class URLHistory {
         public final long visits;
         public final long lastVisit;
 
         private URLHistory(long visits, long lastVisit) {
             this.visits = visits;
             this.lastVisit = lastVisit;
         }
     }
@@ -208,17 +208,17 @@ public class AddToHomeScreenPromotion ex
         return true;
     }
 
     protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(Context context, String url) {
         final UrlAnnotations urlAnnotations = BrowserDB.from(context).getUrlAnnotations();
         return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(context.getContentResolver(), url);
     }
 
-    protected URLHistory getHistoryForURL(Context context, String url) {
+    public static URLHistory getHistoryForURL(Context context, String url) {
         final GeckoProfile profile = GeckoProfile.get(context);
         final BrowserDB browserDB = BrowserDB.from(profile);
 
         Cursor cursor = null;
         try {
             cursor = browserDB.getHistoryForURL(context.getContentResolver(), url);
 
             if (cursor.moveToFirst()) {
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -190,21 +190,28 @@ var Reader = {
     this._showSystemUI(true);
 
     // Only stop a reader session if the foreground viewer is not visible.
     UITelemetry.stopSession("reader.1", "", null);
 
     if (browser.isArticle) {
       showPageAction("drawable://reader", Strings.reader.GetStringFromName("readerView.enter"));
       UITelemetry.addEvent("show.1", "button", null, "reader_available");
+      this._sendMmaEvent("reader_available");
     } else {
       UITelemetry.addEvent("show.1", "button", null, "reader_unavailable");
     }
   },
 
+  _sendMmaEvent: function(event) {
+      WindowEventDispatcher.sendRequest({
+          type: "Mma:"+event,
+      });
+  },
+
   _showSystemUI: function(visibility) {
       WindowEventDispatcher.sendRequest({
           type: "SystemUI:Visibility",
           visible: visibility
       });
   },
 
   /**
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -883,21 +883,25 @@ var BrowserApp = {
       },
       icon: "drawable://ic_menu_share",
       menu: true,
       callback: function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
       }
     });
 
+
     NativeWindow.contextmenus.add(stringGetter("contextmenu.saveImage"),
       NativeWindow.contextmenus.imageSaveableContext,
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
         UITelemetry.addEvent("save.1", "contextmenu", null, "image");
+        WindowEventDispatcher.sendRequest({
+          type: "Mma:web_save_image",
+        });
 
         RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
             if (!permissionGranted) {
                 return;
             }
 
             ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
                                           false, true, aTarget.ownerDocument.documentURIObject,
@@ -933,16 +937,19 @@ var BrowserApp = {
         } else if (aTarget instanceof HTMLAudioElement) {
           return Strings.browser.GetStringFromName("contextmenu.saveAudio");
         }
         return Strings.browser.GetStringFromName("contextmenu.saveVideo");
       }, NativeWindow.contextmenus.mediaSaveableContext,
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
         UITelemetry.addEvent("save.1", "contextmenu", null, "media");
+        WindowEventDispatcher.sendRequest({
+          type: "Mma:web_save_media",
+        });
 
         let url = aTarget.currentSrc || aTarget.src;
 
         let filePickerTitleKey;
         if (aTarget instanceof HTMLVideoElement) {
           if (aTarget.readyState == aTarget.HAVE_NOTHING) {
             filePickerTitleKey = "SaveMediaTitle";
           } else if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0) {
--- a/mobile/android/config/proguard/proguard-leanplum.cfg
+++ b/mobile/android/config/proguard/proguard-leanplum.cfg
@@ -331,17 +331,18 @@
 }
 
 -keep class com.leanplum.utils.BitmapUtil { public private protected *; }
 
 -keep class com.leanplum.LocationManagerImplementation { *; }
 
 -keep class com.leanplum.messagetemplates.BaseMessageOptions { *; }
 
--dontwarn android.support.v7.**
--keep class android.support.v7.** { *; }
--keep interface android.support.v7.** { *; }
+#-dontwarn android.support.v7.**
+-keep class android.support.v7.app.AppCompatActivity
+-keep class android.support.v7.app.ActionBarActivity
+#-keep interface android.support.v7.** { *; }
 
 -printmapping out.map
 -renamesourcefileattribute SourceFile
 -keepattributes SourceFile,LineNumberTable
 
 -optimizations !code/allocation/variable