Bug 1361664 - Part2. Create MVP Triggers and Deeplinks for Mobile Marketing Automatin. r=maliu
☠☠ backed out by 48b319f9d3f2 ☠ ☠
authorNevin Chen <cnevinchen@gmail.com>
Wed, 17 May 2017 01:25:45 +0800
changeset 360806 59dc8710d76835c5219b0a901a3be9214fc55924
parent 360805 f54fb8956671dfe852517bfaac7e173e8d0ae195
child 360807 958964b3117b5a6e6215d1710ddd739090e21b8a
push id31902
push userryanvm@gmail.com
push dateFri, 26 May 2017 19:43:26 +0000
treeherdermozilla-central@ba1a33add29d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmaliu
bugs1361664
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1361664 - Part2. Create MVP Triggers and Deeplinks for Mobile Marketing Automatin. r=maliu This patch do 5 things. They are related so I put them is 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. MozReview-Commit-ID: APEmr1JXBLH
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
--- 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,34 +2,53 @@
 /* -*- 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.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) {
@@ -45,20 +64,39 @@ public class MmaDelegate {
                         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;
+    }
+
+
 }
--- 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,79 @@
 /* -*- 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.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.text.TextUtils;
 
 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;
 
+import static org.mozilla.gecko.mma.MmaDelegate.LAUNCH_BUT_NOT_DEFAULT_BROWSER;
+
 
 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");
         if (installedFocus || installedKlar) {
             attributes.put("focus", "installed");
         }
         Leanplum.start(activity, attributes);
-        Leanplum.track("Launch");
+        if (!isDefaultBrowser(activity)) {
+            Leanplum.track(LAUNCH_BUT_NOT_DEFAULT_BROWSER);
+        }
+
+
         // 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 +88,21 @@ public class MmaLeanplumImp implements M
         Leanplum.track(leanplumEvent, value);
 
     }
 
     @Override
     public void stop() {
         Leanplum.stop();
     }
+
+    private 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/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) {