Bug 818987 - Links to media files should have an 'Open in app' option. r=wesj
authorWes Johnston <wjohnston@mozilla.com>
Tue, 06 Aug 2013 22:13:21 -0700
changeset 141828 a0670c5cac399ccaf65b9f13114ee29a4672e3f2
parent 141827 607b35c777f079c2b097aed34aa0bd528afadece
child 141829 8dac6e09d3d6551b6f8da076f81d0b87343e7c60
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerswesj
bugs818987
milestone26.0a1
Bug 818987 - Links to media files should have an 'Open in app' option. r=wesj
mobile/android/base/GeckoApp.java
mobile/android/chrome/content/HelperApps.js
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -716,16 +716,31 @@ abstract public class GeckoApp
                 } else if (!message.isNull("phone")) {
                     Uri contactUri = Uri.parse(message.getString("phone"));       
                     Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
                     startActivity(i);
                 } else {
                     // something went wrong.
                     Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number");
                 }                
+            } else if (event.equals("Intent:GetHandlers")) {
+                Intent intent = GeckoAppShell.getOpenURIIntent(sAppContext, message.optString("url"),
+                    message.optString("mime"), message.optString("action"), message.optString("title"));
+                String[] handlers = GeckoAppShell.getHandlersForIntent(intent);
+                ArrayList<String> appList = new ArrayList<String>(handlers.length);
+                for (int i = 0; i < handlers.length; i++) {
+                    appList.add(handlers[i]);
+                }
+                JSONObject handlersJSON = new JSONObject();
+                handlersJSON.put("apps", new JSONArray(appList));
+                mCurrentResponse = handlersJSON.toString();
+            } else if (event.equals("Intent:Open")) {
+                GeckoAppShell.openUriExternal(message.optString("url"),
+                    message.optString("mime"), message.optString("packageName"),
+                    message.optString("className"), message.optString("action"), message.optString("title"));
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     public String getResponse(JSONObject origMessage) {
         String res = mCurrentResponse;
@@ -1502,16 +1517,18 @@ abstract public class GeckoApp
         registerEventListener("Share:Image");
         registerEventListener("Image:SetAs");
         registerEventListener("Sanitize:ClearHistory");
         registerEventListener("Update:Check");
         registerEventListener("Update:Download");
         registerEventListener("Update:Install");
         registerEventListener("PrivateBrowsing:Data");
         registerEventListener("Contact:Add");
+        registerEventListener("Intent:Open");
+        registerEventListener("Intent:GetHandlers");
 
         if (SmsManager.getInstance() != null) {
           SmsManager.getInstance().start();
         }
 
         mContactService = new ContactService(GeckoAppShell.getEventDispatcher(), this);
 
         mPromptService = new PromptService(this);
@@ -2056,16 +2073,18 @@ abstract public class GeckoApp
         unregisterEventListener("Share:Image");
         unregisterEventListener("Image:SetAs");
         unregisterEventListener("Sanitize:ClearHistory");
         unregisterEventListener("Update:Check");
         unregisterEventListener("Update:Download");
         unregisterEventListener("Update:Install");
         unregisterEventListener("PrivateBrowsing:Data");
         unregisterEventListener("Contact:Add");
+        unregisterEventListener("Intent:Open");
+        unregisterEventListener("Intent:GetHandlers");
 
         deleteTempFiles();
 
         if (mLayerView != null)
             mLayerView.destroy();
         if (mDoorHangerPopup != null)
             mDoorHangerPopup.destroy();
         if (mFormAssistPopup != null)
--- a/mobile/android/chrome/content/HelperApps.js
+++ b/mobile/android/chrome/content/HelperApps.js
@@ -1,13 +1,26 @@
 /* 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";
 
+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);
+}
+
+function sendMessageToJava(aMessage) {
+  return getBridge().handleGeckoMessage(JSON.stringify(aMessage));
+}
 
 var HelperApps =  {
   get defaultHttpHandlers() {
     let protoHandlers = this.getAppsForProtocol("http");
 
     var results = {};
     for (var i = 0; i < protoHandlers.length; i++) {
       try {
@@ -29,38 +42,64 @@ var HelperApps =  {
     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 handlerInfoProto = this.urlHandlerService.getURLHandlerInfoFromOS(uri, {});
-    let urlHandlers = handlerInfoProto.possibleApplicationHandlers;
-    for (var i = 0; i < urlHandlers.length; i++) {
-      let urlApp = urlHandlers.queryElementAt(i, Ci.nsIHandlerApp);
-      if (!this.defaultHttpHandlers[urlApp.name]) {
-        found.push(urlApp);
-      }
+    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 apps = this._parseApps(JSON.parse(sendMessageToJava(msg)));
+    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;
   },
-  
+
   openUriInApp: function openUriInApp(uri) {
-    var possibleHandlers = this.getAppsForUri(uri);
-    if (possibleHandlers.length == 1) {
-      possibleHandlers[0].launchWithURI(uri);
-    } else if (possibleHandlers.length > 0) {
-      let handlerInfoProto = this.urlHandlerService.getURLHandlerInfoFromOS(uri, {});
-      handlerInfoProto.preferredApplicationHandler.launchWithURI(uri);
+    let mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
+    let msg = {
+      type: "Intent:Open",
+      mime: mimeType,
+      action: "",
+      url: uri.spec,
+      packageName: "",
+      className: ""
+    };
+    sendMessageToJava(msg);
+  },
+
+  _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);
@@ -75,17 +114,17 @@ var HelperApps =  {
     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");
-  
+
     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
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7626,22 +7626,36 @@ let Reader = {
       this.log("Database upgrade done: " + this.DB_VERSION);
     }.bind(this);
   }
 };
 
 var ExternalApps = {
   _contextMenuId: -1,
 
+  // extend _getLink to pickup html5 media links.
+  _getMediaLink: function(aElement) {
+    let uri = NativeWindow.contextmenus._getLink(aElement);
+    if (uri == null) {
+      if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement && mediaSrc)) {
+        try {
+          let mediaSrc = aElement.currentSrc || aElement.src;
+          uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
+        } catch (e) {}
+      }
+    }
+    return uri;
+  },
+
   init: function helper_init() {
     this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
       let uri = null;
       var node = aElement;
       while (node && !uri) {
-        uri = NativeWindow.contextmenus._getLink(node);
+        uri = ExternalApps._getMediaLink(node);
         node = node.parentNode;
       }
       let apps = [];
       if (uri)
         apps = HelperApps.getAppsForUri(uri);
 
       return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
                                 Strings.browser.GetStringFromName("helperapps.openWithList2");
@@ -7649,27 +7663,27 @@ var ExternalApps = {
   },
 
   uninit: function helper_uninit() {
     NativeWindow.contextmenus.remove(this._contextMenuId);
   },
 
   filter: {
     matches: function(aElement) {
-      let uri = NativeWindow.contextmenus._getLink(aElement);
+      let uri = ExternalApps._getMediaLink(aElement);
       let apps = [];
       if (uri) {
         apps = HelperApps.getAppsForUri(uri);
       }
       return apps.length > 0;
     }
   },
 
   openExternal: function(aElement) {
-    let uri = NativeWindow.contextmenus._getLink(aElement);
+    let uri = ExternalApps._getMediaLink(aElement);
     HelperApps.openUriInApp(uri);
   }
 };
 
 var Distribution = {
   // File used to store campaign data
   _file: null,