Bug 1173147 - Show prompt for GeckoAppShell.openUriExternal. r=sebastian
authorMichael Comella <michael.l.comella@gmail.com>
Fri, 18 Sep 2015 17:21:07 -0700
changeset 266952 806d80510c213a12f95c9110be0cf44e16661cee
parent 266951 a3891c6b14abc26d19f3ad573d8c933f8f96fc83
child 266953 efea2819c5bc07fdafa534e27131cc02a9575bd5
push id29503
push usercbook@mozilla.com
push dateFri, 09 Oct 2015 09:36:47 +0000
treeherdermozilla-central@462074ffada4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1173147
milestone44.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 1173147 - Show prompt for GeckoAppShell.openUriExternal. r=sebastian
mobile/android/base/ActivityHandlerHelper.java
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/IntentHelper.java
mobile/android/base/home/HomeFragment.java
mobile/android/base/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
--- a/mobile/android/base/ActivityHandlerHelper.java
+++ b/mobile/android/base/ActivityHandlerHelper.java
@@ -3,30 +3,53 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.ActivityResultHandlerMap;
 
 import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
 import android.content.Intent;
+import android.util.Log;
 
 public class ActivityHandlerHelper {
     private static final String LOGTAG = "GeckoActivityHandlerHelper";
     private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
 
     private static int makeRequestCode(ActivityResultHandler aHandler) {
         return mActivityResultHandlerMap.put(aHandler);
     }
 
     public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
         startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
     }
 
+    /**
+     * Starts the Activity, catching & logging if the Activity fails to start.
+     *
+     * We catch to prevent callers from passing in invalid Intents and crashing the browser.
+     *
+     * @return true if the Activity is successfully started, false otherwise.
+     */
+    public static boolean startIntentAndCatch(final String logtag, final Context context, final Intent intent) {
+        try {
+            context.startActivity(intent);
+            return true;
+        } catch (final ActivityNotFoundException e) {
+            Log.w(logtag, "Activity not found.", e);
+            return false;
+        } catch (final SecurityException e) {
+            Log.w(logtag, "Forbidden to launch activity.", e);
+            return false;
+        }
+    }
+
     public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
         activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
     }
 
 
     public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
         ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
         if (handler != null) {
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1537,17 +1537,17 @@ public class BrowserApp extends GeckoApp
             return;
         }
 
         if (AboutPages.isAboutReader(url)) {
             url = ReaderModeUtils.getUrlFromAboutReader(url);
         }
 
         GeckoAppShell.openUriExternal(url, "text/plain", "", "",
-                                      Intent.ACTION_SEND, tab.getDisplayTitle());
+                                      Intent.ACTION_SEND, tab.getDisplayTitle(), false);
 
         // Context: Sharing via chrome list (no explicit session is active)
         Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST);
     }
 
     private void setToolbarMargin(int margin) {
         ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
         mGeckoLayout.requestLayout();
@@ -1778,17 +1778,17 @@ public class BrowserApp extends GeckoApp
                 public void run() {
                     removeAddonMenuItem(id);
                 }
             });
 
         } else if ("Reader:Share".equals(event)) {
             final String title = message.getString("title");
             final String url = message.getString("url");
-            GeckoAppShell.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title);
+            GeckoAppShell.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
         } else if ("Sanitize:ClearHistory".equals(event)) {
             handleClearHistory(message.optBoolean("clearSearchHistory", false));
             callback.sendSuccess(true);
         } else if ("Sanitize:ClearSyncedTabs".equals(event)) {
             handleClearSyncedTabs();
             callback.sendSuccess(true);
         } else if ("Settings:Show".equals(event)) {
             final String resource =
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -634,17 +634,17 @@ public abstract class GeckoApp
             String text = message.getString("text");
             final Tab tab = Tabs.getInstance().getSelectedTab();
             String title = "";
             if (tab != null) {
                 title = tab.getDisplayTitle();
                 final String url = ReaderModeUtils.stripAboutReaderUrl(tab.getURL());
                 text += "\n\n" + url;
             }
-            GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title);
+            GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false);
 
             // Context: Sharing via chrome list (no explicit session is active)
             Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST);
 
         } else if ("SystemUI:Visibility".equals(event)) {
             setSystemUiVisible(message.getBoolean("visible"));
 
         } else if ("Toast:Show".equals(event)) {
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -50,23 +50,23 @@ import org.mozilla.gecko.util.GeckoReque
 import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSContainer;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ProxySelector;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -101,16 +101,17 @@ import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.SystemClock;
 import android.os.Vibrator;
 import android.provider.Browser;
 import android.provider.Settings;
+import android.support.v4.app.FragmentActivity;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
 import android.view.HapticFeedbackConstants;
@@ -1044,38 +1045,54 @@ public class GeckoAppShell
             final String number = aUri.getSchemeSpecificPart();
             if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
                 return false;
             }
         }
         return true;
     }
 
+    @WrapForJNI
+    public static boolean openUriExternal(String targetURI,
+                                          String mimeType,
+                                          String packageName,
+                                          String className,
+                                          String action,
+                                          String title) {
+        // Default to showing prompt in private browsing to be safe.
+        return openUriExternal(targetURI, mimeType, packageName, className, action, title, true);
+    }
+
     /**
      * Given the inputs to <code>getOpenURIIntent</code>, plus an optional
      * package name and class name, create and fire an intent to open the
      * provided URI. If a class name is specified but a package name is not,
      * we will default to using the current fennec package.
      *
      * @param targetURI the string spec of the URI to open.
      * @param mimeType an optional MIME type string.
      * @param packageName an optional app package name.
      * @param className an optional intent class name.
      * @param action an Android action specifier, such as
      *               <code>Intent.ACTION_SEND</code>.
      * @param title the title to use in <code>ACTION_SEND</code> intents.
-     * @return true if the activity started successfully; false otherwise.
+     * @param showPromptInPrivateBrowsing whether or not the user should be prompted when opening
+     *                                    this uri from private browsing. This should be true
+     *                                    when the user doesn't explicitly choose to open an an
+     *                                    external app (e.g. just clicked a link).
+     * @return true if the activity started successfully or the user was prompted to open the
+     *              application; false otherwise.
      */
-    @WrapForJNI
     public static boolean openUriExternal(String targetURI,
                                           String mimeType,
                                           String packageName,
                                           String className,
                                           String action,
-                                          String title) {
+                                          String title,
+                                          final boolean showPromptInPrivateBrowsing) {
         final Context context = getContext();
         final Intent intent = getOpenURIIntent(context, targetURI,
                                                mimeType, action, title);
 
         if (intent == null) {
             return false;
         }
 
@@ -1084,25 +1101,26 @@ public class GeckoAppShell
                 intent.setClassName(packageName, className);
             } else {
                 // Default to using the fennec app context.
                 intent.setClassName(context, className);
             }
         }
 
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        try {
-            context.startActivity(intent);
-            return true;
-        } catch (ActivityNotFoundException e) {
-            Log.w(LOGTAG, "Activity not found.", e);
-            return false;
-        } catch (SecurityException e) {
-            Log.w(LOGTAG, "Forbidden to launch activity.", e);
-            return false;
+
+        if (!showPromptInPrivateBrowsing) {
+            return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
+        } else {
+            // Ideally we retrieve the Activity from the calling args, rather than
+            // statically, but since this method is called from Gecko and I'm
+            // unfamiliar with that code, this is a simpler solution.
+            final FragmentActivity fragmentActivity = (FragmentActivity) getGeckoInterface().getActivity();
+            return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
+                    context, fragmentActivity.getSupportFragmentManager(), intent);
         }
     }
 
     /**
      * Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
      * but with a guaranteed-lowercase scheme as if the API level 16 method
      * <code>u.normalizeScheme</code> had been called.
      *
--- a/mobile/android/base/IntentHelper.java
+++ b/mobile/android/base/IntentHelper.java
@@ -118,17 +118,17 @@ public final class IntentHelper implemen
     }
 
     private void open(JSONObject message) throws JSONException {
         GeckoAppShell.openUriExternal(message.optString("url"),
                                       message.optString("mime"),
                                       message.optString("packageName"),
                                       message.optString("className"),
                                       message.optString("action"),
-                                      message.optString("title"));
+                                      message.optString("title"), false);
     }
 
     private void openForResult(final JSONObject message) throws JSONException {
         Intent intent = GeckoAppShell.getOpenURIIntent(activity,
                                                        message.optString("url"),
                                                        message.optString("mime"),
                                                        message.optString("action"),
                                                        message.optString("title"));
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -192,17 +192,17 @@ public abstract class HomeFragment exten
         }
 
         if (itemId == R.id.home_share) {
             if (info.url == null) {
                 Log.e(LOGTAG, "Can't share because URL is null");
                 return false;
             } else {
                 GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
-                                              Intent.ACTION_SEND, info.getDisplayTitle());
+                                              Intent.ACTION_SEND, info.getDisplayTitle(), false);
 
                 // Context: Sharing via chrome homepage contextmenu list (home session should be active)
                 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST);
                 return true;
             }
         }
 
         if (itemId == R.id.home_add_to_launcher) {
--- a/mobile/android/base/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
+++ b/mobile/android/base/widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java
@@ -1,14 +1,15 @@
 // 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.widget;
 
+import org.mozilla.gecko.ActivityHandlerHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 
 import android.app.Dialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -50,38 +51,43 @@ public class ExternalIntentDuringPrivate
                     public void onClick(final DialogInterface dialog, final int id) {
                         context.startActivity(intent);
                     }
                 })
                 .setNegativeButton(R.string.button_no, null /* we do nothing if the user rejects */ );
         return builder.create();
     }
 
-    public static void showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager,
+    /**
+     * @return true if the Activity is started or a dialog is shown. false if the Activity fails to start.
+     */
+    public static boolean showDialogOrAndroidChooser(final Context context, final FragmentManager fragmentManager,
             final Intent intent) {
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
         if (selectedTab == null || !selectedTab.isPrivate()) {
-            context.startActivity(intent);
-            return;
+            return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
         }
 
         final PackageManager pm = context.getPackageManager();
         final List<ResolveInfo> matchingActivities = pm.queryIntentActivities(intent, 0);
         if (matchingActivities.size() == 1) {
             final ExternalIntentDuringPrivateBrowsingPromptFragment fragment = new ExternalIntentDuringPrivateBrowsingPromptFragment();
 
             final Bundle args = new Bundle(2);
             args.putCharSequence(KEY_APPLICATION_NAME, matchingActivities.get(0).loadLabel(pm));
             args.putParcelable(KEY_INTENT, intent);
             fragment.setArguments(args);
 
             fragment.show(fragmentManager, FRAGMENT_TAG);
+            // We don't know the results of the user interaction with the fragment so just return true.
+            return true;
         } else if (matchingActivities.size() > 1) {
             // Android chooser dialog will be shown, which should make the users
             // aware they're entering a new application from private browsing.
-            context.startActivity(intent);
+            return ActivityHandlerHelper.startIntentAndCatch(LOGTAG, context, intent);
         } else {
             // Normally, we show about:neterror when an Intent does not resolve
             // but we don't have the references here to do that so log instead.
             Log.w(LOGTAG, "showDialogOrAndroidChooser unexpectedly called with Intent that does not resolve");
+            return false;
         }
     }
 }