Bug 1168980 - Search app store when there is no app to handle intent:// URI. r=margaret
authorMichael Comella <michael.l.comella@gmail.com>
Tue, 16 Jun 2015 11:39:00 -0700
changeset 267267 9bae6f3fdfb26e6ffa54f9e2651eb03550019036
parent 267266 f62212fd6af6b4283ed2f75b7161e35ef5b830b0
child 267268 cec9d0203f28f4a56a6b80878700a214a92ac9b3
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-esr52@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs1168980
milestone41.0a1
Bug 1168980 - Search app store when there is no app to handle intent:// URI. r=margaret
mobile/android/base/IntentHelper.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/strings.xml.in
mobile/android/components/ContentDispatchChooser.js
mobile/android/locales/en-US/chrome/handling.properties
--- a/mobile/android/base/IntentHelper.java
+++ b/mobile/android/base/IntentHelper.java
@@ -11,29 +11,39 @@ import org.mozilla.gecko.util.JSONUtils;
 import org.mozilla.gecko.util.WebActivityMapper;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.app.Activity;
 import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
 import android.util.Log;
+import android.widget.Toast;
 
+import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.List;
 
 public final class IntentHelper implements GeckoEventListener {
     private static final String LOGTAG = "GeckoIntentHelper";
     private static final String[] EVENTS = {
         "Intent:GetHandlers",
         "Intent:Open",
         "Intent:OpenForResult",
+        "Intent:OpenNoHandler",
         "WebActivity:Open"
     };
+
+    // via http://developer.android.com/distribute/tools/promote/linking.html
+    private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
+    private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
+
     private static IntentHelper instance;
 
     private final Activity activity;
 
     private IntentHelper(Activity activity) {
         this.activity = activity;
         EventDispatcher.getInstance().registerGeckoThreadListener(this, EVENTS);
     }
@@ -59,16 +69,18 @@ public final class IntentHelper implemen
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("Intent:GetHandlers")) {
                 getHandlers(message);
             } else if (event.equals("Intent:Open")) {
                 open(message);
             } else if (event.equals("Intent:OpenForResult")) {
                 openForResult(message);
+            } else if (event.equals("Intent:OpenNoHandler")) {
+                openNoHandler(message);
             } else if (event.equals("WebActivity:Open")) {
                 openWebActivity(message);
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
@@ -106,16 +118,76 @@ public final class IntentHelper implemen
         final ResultHandler handler = new ResultHandler(message);
         try {
             ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
         } catch (SecurityException e) {
             Log.w(LOGTAG, "Forbidden to launch activity.", e);
         }
     }
 
+    /**
+     * Opens a URI without any valid handlers on device. In the best case, a package is specified
+     * and we can bring the user directly to the application page in an app market. If a package is
+     * not specified and there is a fallback url in the intent extras, we open that url. If neither
+     * is present, we alert the user that we were unable to open the link.
+     */
+    private void openNoHandler(final JSONObject msg) {
+        final String uri = msg.optString("uri");
+
+        if (TextUtils.isEmpty(uri)) {
+            displayToastCannotOpenLink();
+            Log.w(LOGTAG, "Received empty URL. Ignoring...");
+            return;
+        }
+
+        final Intent intent;
+        try {
+            // TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
+            intent = Intent.parseUri(uri, 0);
+        } catch (final URISyntaxException e) {
+            displayToastCannotOpenLink();
+            // Don't log the exception to prevent leaking URIs.
+            Log.w(LOGTAG, "Unable to parse Intent URI");
+            return;
+        }
+
+        // For this flow, we follow Chrome's lead:
+        //   https://developer.chrome.com/multidevice/android/intents
+        //
+        // Note on alternative flows: we could get the intent package from a component, however, for
+        // security reasons, components are ignored when opening URIs (bug 1168998) so we should
+        // ignore it here too.
+        //
+        // Our old flow used to prompt the user to search for their app in the market by scheme and
+        // while this could help the user find a new app, there is not always a correlation in
+        // scheme to application name and we could end up steering the user wrong (potentially to
+        // malicious software). Better to leave that one alone.
+        if (intent.getPackage() != null) {
+            final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
+            final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
+            marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
+            marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            activity.startActivity(marketIntent);
+
+        } else if (intent.hasExtra(EXTRA_BROWSER_FALLBACK_URL)) {
+            final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
+            Tabs.getInstance().loadUrl(fallbackUrl);
+
+        }  else {
+            displayToastCannotOpenLink();
+            // Don't log the URI to prevent leaking it.
+            Log.w(LOGTAG, "Unable to handle URI");
+        }
+    }
+
+    private void displayToastCannotOpenLink() {
+        final String errText = activity.getResources().getString(R.string.intent_uri_cannot_open);
+        Toast.makeText(activity, errText, Toast.LENGTH_LONG).show();
+    }
+
     private void openWebActivity(JSONObject message) throws JSONException {
         final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
         ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
     }
 
     private static class ResultHandler implements ActivityResultHandler {
         private final JSONObject message;
 
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -654,8 +654,10 @@ just addresses the organization to follo
 <!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
 <!-- Localization note: Used when the sync has not happend yet, showed in place of a date -->
 <!ENTITY remote_tabs_never_synced "Last synced: never">
 
 <!-- Find-In-Page strings -->
 <!-- LOCALIZATION NOTE (find_matchcase): This is meant to appear as an icon that changes color
      if match-case is activated. i.e. No more than two letters, one uppercase, one lowercase. -->
 <!ENTITY find_matchcase "Aa">
+
+<!ENTITY intent_uri_cannot_open "Cannot open link">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -533,9 +533,11 @@
   <!-- Miscellaneous -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
   <string name="percent">&percent;</string>
 
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
+
+  <string name="intent_uri_cannot_open">&intent_uri_cannot_open;</string>
 </resources>
--- a/mobile/android/components/ContentDispatchChooser.js
+++ b/mobile/android/components/ContentDispatchChooser.js
@@ -44,33 +44,27 @@ ContentDispatchChooser.prototype =
     // specific results.
     aHandler = this.protoSvc.getProtocolHandlerInfoFromOS(aURI.spec, {});
 
     // The first handler in the set is the Android Application Chooser (which will fall back to a default if one is set)
     // If we have more than one option, let the OS handle showing a list (if needed).
     if (aHandler.possibleApplicationHandlers.length > 1) {
       aHandler.launchWithURI(aURI, aWindowContext);
     } else {
+      // xpcshell tests do not have an Android Bridge but we require Android
+      // Bridge when using Messaging so we guard against this case. xpcshell
+      // tests also do not have a window, so we use this state to guard.
       let win = this._getChromeWin();
-      if (win && win.NativeWindow) {
-        let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
-        let failedText = bundle.GetStringFromName("protocol.failed");
-        let searchText = bundle.GetStringFromName("protocol.toast.search");
+      if (!win) {
+        return;
+      }
 
-        win.NativeWindow.toast.show(failedText, "long", {
-          button: {
-            label: searchText,
-            callback: function() {
-              let message = {
-                type: "Intent:Open",
-                url: "market://search?q=" + aURI.scheme,
-              };
+      let msg = {
+        type: "Intent:OpenNoHandler",
+        uri: aURI.spec,
+      };
 
-              Messaging.sendRequest(message);
-            }
-          }
-        });
-      }
+      Messaging.sendRequest(msg);
     }
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]);
--- a/mobile/android/locales/en-US/chrome/handling.properties
+++ b/mobile/android/locales/en-US/chrome/handling.properties
@@ -1,8 +1,5 @@
 # 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/.
 
 download.blocked=Unable to download file
-protocol.failed=Couldn't find an app to open this link
-# A very short string shown in the button toast when no application can open the url
-protocol.toast.search=Search