Bug 780379 - Show a custom intent chooser for downloads that can be downloaded by an external app. r=mfinkle
authorWes Johnston <wjohnston@mozilla.com>
Thu, 17 Oct 2013 16:30:47 -0700
changeset 166166 0cc4a019835f096a5df82d9070db9b96addc75d6
parent 166165 f0208c342805e4b88aabf142bd97c6d8a6696596
child 166167 aec9897dc66dbeca75576f9958e2797550a8b61e
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs780379
milestone27.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 780379 - Show a custom intent chooser for downloads that can be downloaded by an external app. r=mfinkle
mobile/android/chrome/content/HelperApps.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/components/HelperAppDialog.js
mobile/android/locales/en-US/chrome/browser.properties
mobile/android/modules/HelperApps.jsm
mobile/android/modules/moz.build
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -48,26 +48,28 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                   "resource://gre/modules/Sanitizer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
+                                  "resource://gre/modules/HelperApps.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 // Lazily-loaded browser scripts:
 [
-  ["HelperApps", "chrome://browser/content/HelperApps.js"],
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
   ["AboutReader", "chrome://browser/content/aboutReader.js"],
   ["WebAppRT", "chrome://browser/content/WebAppRT.js"],
   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
   ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
   ["Linkifier", "chrome://browser/content/Linkify.js"],
@@ -2868,17 +2870,17 @@ Tab.prototype = {
     if (!this.browser || !this.browser.docShell)
       return;
 
     if (aActive) {
       this.browser.setAttribute("type", "content-primary");
       this.browser.focus();
       this.browser.docShellIsActive = true;
       Reader.updatePageAction(this);
-      HelperApps.updatePageAction(this.browser.currentURI);
+      ExternalApps.updatePageAction(this.browser.currentURI);
     } else {
       this.browser.setAttribute("type", "content-targetable");
       this.browser.docShellIsActive = false;
     }
   },
 
   getActive: function getActive() {
     return this.browser.docShellIsActive;
@@ -3578,17 +3580,17 @@ Tab.prototype = {
         if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
           if (!this._linkifier)
             this._linkifier = new Linkifier();
           this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
         }
 
         // Show page actions for helper apps.
         if (BrowserApp.selectedTab == this)
-          HelperApps.updatePageAction(this.browser.currentURI);
+          ExternalApps.updatePageAction(this.browser.currentURI);
 
         if (!Reader.isEnabledForParseOnLoad)
           return;
 
         // Once document is fully loaded, parse it
         Reader.parseDocumentFromTab(this.id, function (article) {
           // Do nothing if there's no article or the page in this tab has
           // changed
@@ -7750,17 +7752,70 @@ var ExternalApps = {
       }
       return apps.length > 0;
     }
   },
 
   openExternal: function(aElement) {
     let uri = ExternalApps._getMediaLink(aElement);
     HelperApps.openUriInApp(uri);
-  }
+  },
+
+  updatePageAction: function updatePageAction(uri) {
+    let apps = HelperApps.getAppsForUri(uri);
+
+    if (apps.length > 0)
+      this._setUriForPageAction(uri, apps);
+    else
+      this._removePageAction();
+  },
+
+  _setUriForPageAction: function setUriForPageAction(uri, apps) {
+    this._pageActionUri = uri;
+
+    // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
+    if (this._pageActionId != undefined)
+      return;
+
+    this._pageActionId = NativeWindow.pageactions.add({
+      title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+      icon: "drawable://icon_openinapp",
+      clickCallback: (function() {
+        let callback = function(app) {
+          app.launch(uri);
+        }
+
+        if (apps.length > 1) {
+          // Use the HelperApps prompt here to filter out any Http handlers
+          HelperApps.prompt(apps, {
+            title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+            buttons: [
+              Strings.browser.GetStringFromName("openInApp.ok"),
+              Strings.browser.GetStringFromName("openInApp.cancel")
+            ]
+          }, function(result) {
+            if (result.button != 0)
+              return;
+
+            callback(apps[result.icongrid0]);
+          });
+        } else {
+          callback(apps[0]);
+        }
+      }).bind(this)
+    });
+  },
+
+  _removePageAction: function removePageAction() {
+    if(!this._pageActionId)
+      return;
+
+    NativeWindow.pageactions.remove(this._pageActionId);
+    delete this._pageActionId;
+  },
 };
 
 var Distribution = {
   // File used to store campaign data
   _file: null,
 
   init: function dc_init() {
     Services.obs.addObserver(this, "Distribution:Set", false);
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -31,17 +31,16 @@ chrome.jar:
 * content/browser.js                   (content/browser.js)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
   content/bindings/settings.xml        (content/bindings/settings.xml)
   content/exceptions.js                (content/exceptions.js)
   content/downloads.js                 (content/downloads.js)
   content/netError.xhtml               (content/netError.xhtml)
   content/SelectHelper.js              (content/SelectHelper.js)
   content/SelectionHandler.js          (content/SelectionHandler.js)
-  content/HelperApps.js                (content/HelperApps.js)
   content/dbg-browser-actors.js        (content/dbg-browser-actors.js)
   content/WebAppRT.js                  (content/WebAppRT.js)
   content/InputWidgetHelper.js         (content/InputWidgetHelper.js)
   content/WebrtcUI.js                  (content/WebrtcUI.js)
   content/MemoryObserver.js            (content/MemoryObserver.js)
   content/ConsoleAPI.js                (content/ConsoleAPI.js)
   content/PluginHelper.js              (content/PluginHelper.js)
   content/OfflineApps.js               (content/OfflineApps.js)
--- a/mobile/android/components/HelperAppDialog.js
+++ b/mobile/android/components/HelperAppDialog.js
@@ -1,37 +1,70 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 /* 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/. */
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
 const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Prompt.jsm");
+Cu.import("resource://gre/modules/HelperApps.jsm");
 
 // -----------------------------------------------------------------------
 // HelperApp Launcher Dialog
 // -----------------------------------------------------------------------
 
 function HelperAppLauncherDialog() { }
 
 HelperAppLauncherDialog.prototype = {
   classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
 
   show: function hald_show(aLauncher, aContext, aReason) {
-    // Save everything by default
-    aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
-    aLauncher.saveToDisk(null, false);
+    let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+    let defaultHandler = new Object();
+    let apps = HelperApps.getAppsForUri(aLauncher.source, {
+      mimeType: aLauncher.MIMEInfo.MIMEType,
+    });
+
+    // Add a fake intent for save to disk at the top of the list
+    apps.unshift({
+      name: bundle.GetStringFromName("helperapps.saveToDisk"),
+      iconUri: "drawable://icon",
+      launch: function() {
+        // Reset the preferredAction here
+        aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+        aLauncher.saveToDisk(null, false);
+        return true;
+      }
+    });
+
+    let app = apps[0];
+    if (apps.length > 1) {
+      app = HelperApps.prompt(apps, {
+        title: bundle.GetStringFromName("helperapps.pick")
+      });
+    }
+
+    if (app) {
+      aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+      if (!app.launch(aLauncher.source)) {
+        aLauncher.cancel();
+      }
+    } else {
+      // Something weird happened. Log an error
+      Services.console.logStringMessage("Unexpected selection from grid: " + app);
+    }
+
   },
 
   promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) {
     // Retrieve the user's default download directory
     let dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
     let defaultFolder = dnldMgr.userDownloadsDirectory;
 
     try {
@@ -58,17 +91,17 @@ HelperAppLauncherDialog.prototype = {
   },
 
   makeFileUnique: function hald_makeFileUnique(aLocalFile) {
     try {
       // Note - this code is identical to that in
       //   toolkit/content/contentAreaUtils.js.
       // If you are updating this code, update that code too! We can't share code
       // here since this is called in a js component.
-      var collisionCount = 0;
+      let collisionCount = 0;
       while (aLocalFile.exists()) {
         collisionCount++;
         if (collisionCount == 1) {
           // Append "(2)" before the last dot in (or at the end of) the filename
           // special case .ext.gz etc files so we don't wind up with .tar(2).gz
           if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
             aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
           else
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -258,16 +258,18 @@ remoteIncomingPromptCancel=Cancel
 # Helper apps
 helperapps.open=Open
 helperapps.ignore=Ignore
 helperapps.dontAskAgain=Don't ask again for this site
 helperapps.openWithApp2=Open With %S App
 helperapps.openWithList2=Open With an App
 helperapps.always=Always
 helperapps.never=Never
+helperapps.pick=Complete action using
+helperapps.saveToDisk=Download
 
 #Lightweight themes
 # LOCALIZATION NOTE (lwthemeInstallRequest.message): %S will be replaced with
 # the host name of the site.
 lwthemeInstallRequest.message=This site (%S) attempted to install a theme.
 lwthemeInstallRequest.allowButton=Allow
 
 # LOCALIZATION NOTE (getUserMedia.shareCamera.message, getUserMedia.shareMicrophone.message, getUserMedia.shareCameraAndMicrophone.message, getUserMedia.sharingCamera.message, getUserMedia.sharingMicrophone.message, getUserMedia.sharingCameraAndMicrophone.message): %S is the website origin (e.g. www.mozilla.org)
@@ -289,8 +291,11 @@ getUserMedia.sharingMicrophone.message2 
 getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on
 
 #Reader mode
 readerMode.enter = Enter Reader Mode
 readerMode.exit = Exit Reader Mode
 
 #Open in App
 openInApp.pageAction = Open in App
+openInApp.ok = OK
+openInApp.cancel = Cancel
+
rename from mobile/android/chrome/content/HelperApps.js
rename to mobile/android/modules/HelperApps.jsm
--- a/mobile/android/chrome/content/HelperApps.js
+++ b/mobile/android/modules/HelperApps.jsm
@@ -1,196 +1,139 @@
 /* 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/. */
 "use strict";
 
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Prompt.jsm");
+
 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
   let ContentAreaUtils = {};
   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
   return ContentAreaUtils;
 });
 
-function getBridge() {
-  return Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge);
+this.EXPORTED_SYMBOLS = ["App","HelperApps"];
+
+function App(data) {
+  this.name = data.name;
+  this.isDefault = data.isDefault;
+  this.packageName = data.packageName;
+  this.activityName = data.activityName;
+  this.iconUri = "-moz-icon://" + data.packageName;
 }
 
-function sendMessageToJava(aMessage) {
-  return getBridge().handleGeckoMessage(JSON.stringify(aMessage));
+App.prototype = {
+  launch: function(uri) {
+    HelperApps._launchApp(this, uri);
+    return false;
+  }
 }
 
 var HelperApps =  {
   get defaultHttpHandlers() {
-    let protoHandlers = this.getAppsForProtocol("http");
-
-    var results = {};
-    for (var i = 0; i < protoHandlers.length; i++) {
-      try {
-        let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
-        results[protoApp.name] = protoApp;
-      } catch(e) {}
-    }
-
     delete this.defaultHttpHandlers;
-    return this.defaultHttpHandlers = results;
+    return this.defaultHttpHandlers = this.getAppsForProtocol("http");
   },
 
   get protoSvc() {
     delete this.protoSvc;
     return this.protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
   },
 
   get urlHandlerService() {
     delete this.urlHandlerService;
     return this.urlHandlerService = Cc["@mozilla.org/uriloader/external-url-handler-service;1"].getService(Ci.nsIExternalURLHandlerService);
   },
 
-  getAppsForProtocol: function getAppsForProtocol(uri) {
-    let handlerInfoProto = this.protoSvc.getProtocolHandlerInfoFromOS(uri, {});
-    return handlerInfoProto.possibleApplicationHandlers;
-  },
-
-  getAppsForUri: function getAppsFor(uri) {
-    let found = [];
-    let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
-    // empty action string defaults to android.intent.action.VIEW
-    let msg = {
-      type: "Intent:GetHandlers",
-      mime: mimeType,
-      action: "",
-      url: uri.spec,
-      packageName: "",
-      className: ""
-    };
-    let data = sendMessageToJava(msg);
-    if (!data)
-      return found;
-
-    let apps = this._parseApps(JSON.parse(data));
-    for (let i = 0; i < apps.length; i++) {
-      let appName = apps[i].name;
-      if (appName.length > 0 && !this.defaultHttpHandlers[appName])
-        found.push(apps[i]);
-    }
-    return found;
-  },
-
-  updatePageAction: function setPageAction(uri) {
-    let apps = this.getAppsForUri(uri);
-    if (apps.length > 0)
-      this._setPageActionFor(uri, apps);
-    else
-      this._removePageAction();
+  prompt: function showPicker(apps, promptOptions, callback) {
+    let p = new Prompt(promptOptions).addIconGrid({ items: apps });
+    p.show(callback);
   },
 
-  _setPageActionFor: function setPageActionFor(uri, apps) {
-    this._pageActionUri = uri;
-
-    // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
-    if (this._pageActionId != undefined)
-      return;
+  getAppsForProtocol: function getAppsForProtocol(scheme) {
+    let protoHandlers = this.protoSvc.getProtocolHandlerInfoFromOS(scheme, {}).possibleApplicationHandlers;
 
-    this._pageActionId = NativeWindow.pageactions.add({
-      title: Strings.browser.GetStringFromName("openInApp.pageAction"),
-      icon: "drawable://icon_openinapp",
-      clickCallback: function() {
-        if (apps.length == 1)
-          this._launchApp(apps[0], this._pageActionUri);
-        else
-          this.openUriInApp(this._pageActionUri);
-      }.bind(this)
-    });
-  },
+    let results = {};
+    for (let i = 0; i < protoHandlers.length; i++) {
+      try {
+        let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+        results[protoApp.name] = new App({
+          name: protoApp.name,
+          description: protoApp.detailedDescription,
+        });
+      } catch(e) {}
+    }
 
-  _removePageAction: function removePageAction() {
-    if(!this._pageActionId)
-      return;
-
-    NativeWindow.pageactions.remove(this._pageActionId);
-    delete this._pageActionId;
+    return results;
   },
 
-  _launchApp: function launchApp(appData, uri) {
-    let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
-    let msg = {
-      type: "Intent:Open",
-      mime: mimeType,
-      action: "android.intent.action.VIEW",
-      url: uri.spec,
-      packageName: appData.packageName,
-      className: appData.activityName
-    };
-    sendMessageToJava(msg);
-  },
+  getAppsForUri: function getAppsForUri(uri, flags = { filterHttp: true }) {
+    flags.filterHttp = "filterHttp" in flags ? flags.filterHttp : true;
+
+    // Query for apps that can/can't handle the mimetype
+    let msg = this._getMessage("Intent:GetHandlers", uri, flags);
+    let apps = this._parseApps(this._sendMessage(msg).apps);
 
-  openUriInApp: function openUriInApp(uri) {
-    let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
-    let msg = {
-      type: "Intent:Open",
-      mime: mimeType,
-      action: "",
-      url: uri.spec,
-      packageName: "",
-      className: ""
-    };
-    sendMessageToJava(msg);
-  },
+    if (flags.filterHttp) {
+      apps = apps.filter(function(app) {
+        return app.name && !this.defaultHttpHandlers[app.name];
+      }, this);
+    }
 
-  _parseApps: function _parseApps(aJSON) {
-    // aJSON -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
-    // see GeckoAppShell.java getHandlersForIntent function for details
-    let appInfo = aJSON.apps;
-    const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
-    let apps = [];
-    for (let i = 0; i < appInfo.length; i += numAttr) {
-      apps.push({"name" : appInfo[i],
-                 "isDefault" : appInfo[i+1],
-                 "packageName" : appInfo[i+2],
-                 "activityName" : appInfo[i+3]});
-    }
     return apps;
   },
 
-  showDoorhanger: function showDoorhanger(aUri, aCallback) {
-    let permValue = Services.perms.testPermission(aUri, "native-intent");
-    if (permValue != Services.perms.UNKNOWN_ACTION) {
-      if (permValue == Services.perms.ALLOW_ACTION) {
-        if (aCallback)
-          aCallback(aUri);
-        else
-          this.openUriInApp(aUri);
-      } else if (permValue == Services.perms.DENY_ACTION) {
-        // do nothing
-      }
-      return;
+  launchUri: function launchUri(uri) {
+    let msg = this._getMessage("Intent:Open", uri);
+    this._sendMessage(msg);
+  },
+
+  _parseApps: function _parseApps(appInfo) {
+    // appInfo -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
+    // see GeckoAppShell.java getHandlersForIntent function for details
+    const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
+
+    let apps = [];
+    for (let i = 0; i < appInfo.length; i += numAttr) {
+      apps.push(new App({"name" : appInfo[i],
+                 "isDefault" : appInfo[i+1],
+                 "packageName" : appInfo[i+2],
+                 "activityName" : appInfo[i+3]}));
     }
 
-    let apps = this.getAppsForUri(aUri);
-    let strings = Strings.browser;
-
-    let message = "";
-    if (apps.length == 1)
-      message = strings.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1);
-    else
-      message = strings.GetStringFromName("helperapps.openWithList2");
+    return apps;
+  },
 
-    let buttons = [{
-      label: strings.GetStringFromName("helperapps.open"),
-      callback: function(aChecked) {
-        if (aChecked)
-          Services.perms.add(aUri, "native-intent", Ci.nsIPermissionManager.ALLOW_ACTION);
-        if (aCallback)
-          aCallback(aUri);
-        else
-          this.openUriInApp(aUri);
-      }
-    }, {
-      label: strings.GetStringFromName("helperapps.ignore"),
-      callback: function(aChecked) {
-        if (aChecked)
-          Services.perms.add(aUri, "native-intent", Ci.nsIPermissionManager.DENY_ACTION);
-      }
-    }];
+  _getMessage: function(type, uri, options = {}) {
+    let mimeType = options.mimeType;
+    if (uri && mimeType == undefined)
+      mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
+      
+    return {
+      type: type,
+      mime: mimeType,
+      action: options.action || "", // empty action string defaults to android.intent.action.VIEW
+      url: uri ? uri.spec : "",
+      packageName: options.packageName || "",
+      className: options.className || ""
+    };
+  },
 
-    let options = { checkbox: Strings.browser.GetStringFromName("helperapps.dontAskAgain") };
-    NativeWindow.doorhanger.show(message, "helperapps-open", buttons, BrowserApp.selectedTab.id, options);
-  }
+  _launchApp: function launchApp(app, uri) {
+    let msg = this._getMessage("Intent:Open", uri, {
+      packageName: app.packageName,
+      className: app.activityName
+    });
+
+    this._sendMessage(msg);
+  },
+
+  _sendMessage: function(msg) {
+    Services.console.logStringMessage("Sending: " + JSON.stringify(msg));
+    let res = Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
+    return JSON.parse(res);
+  },
 };
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES += [
     'ContactService.jsm',
+    'HelperApps.jsm',
     'Home.jsm',
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'OrderedBroadcast.jsm',
     'Prompt.jsm',
     'Sanitizer.jsm',
     'SharedPreferences.jsm',
 ]