Backed out changeset 462e9e4314ef (bug 1258450)
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 30 Mar 2016 14:02:12 +0200
changeset 290871 d085c3aaf69df9f799340339a4f73eb4cb8973f5
parent 290870 7f7a84327477419e181f0369cebd26678cee2e88
child 290872 353d5e668129eb0e808abed59aff1976c18b8271
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1258450
milestone48.0a1
backs out462e9e4314eff25e00e65e75359274ec09807eee
Backed out changeset 462e9e4314ef (bug 1258450)
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -3499,17 +3499,17 @@ public class BrowserApp extends GeckoApp
                 if (url != null) {
                     if (AboutPages.isAboutReader(url)) {
                         url = ReaderModeUtils.getUrlFromAboutReader(url);
                     }
 
                     // Context: Sharing via chrome list (no explicit session is active)
                     Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
 
-                    IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false);
+                    GeckoAppShell.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false);
                 }
             }
             return true;
         }
 
         if (itemId == R.id.reload) {
             tab = Tabs.getInstance().getSelectedTab();
             if (tab != null)
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko;
 
 import android.content.ContentResolver;
 import android.widget.AdapterView;
 import android.widget.Button;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
+import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadataTable;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.FullScreenState;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
@@ -55,16 +56,17 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.location.Location;
 import android.location.LocationListener;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -87,16 +89,17 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.widget.AbsoluteLayout;
 import android.widget.FrameLayout;
 import android.widget.ListView;
 import android.widget.RelativeLayout;
 import android.widget.SimpleAdapter;
 import android.widget.TextView;
@@ -631,17 +634,17 @@ public abstract class GeckoApp
 
         } else if ("Share:Text".equals(event)) {
             final String text = message.getString("text");
             final Tab tab = Tabs.getInstance().getSelectedTab();
             String title = "";
             if (tab != null) {
                 title = tab.getDisplayTitle();
             }
-            IntentHelper.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false);
+            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, "text");
 
         } else if ("Snackbar:Show".equals(event)) {
             SnackbarHelper.showSnackbar(this, message, callback);
         } else if ("SystemUI:Visibility".equals(event)) {
             setSystemUiVisible(message.getBoolean("visible"));
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -10,16 +10,17 @@ import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.net.MalformedURLException;
 import java.net.Proxy;
+import java.net.URISyntaxException;
 import java.net.URLConnection;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.StringTokenizer;
@@ -29,28 +30,31 @@ import java.util.concurrent.ConcurrentHa
 import android.annotation.SuppressLint;
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PanZoomController;
+import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoRequest;
 import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 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.Manifest;
+import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
@@ -80,17 +84,19 @@ import android.location.LocationManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Looper;
 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.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
 import android.view.HapticFeedbackConstants;
 import android.view.Surface;
@@ -830,31 +836,31 @@ public class GeckoAppShell
                 default:
                     return 72;
             }
         }
     }
 
     @WrapForJNI(stubName = "GetHandlersForMimeTypeWrapper")
     static String[] getHandlersForMimeType(String aMimeType, String aAction) {
-        Intent intent = IntentHelper.getIntentForActionString(aAction);
+        Intent intent = getIntentForActionString(aAction);
         if (aMimeType != null && aMimeType.length() > 0)
             intent.setType(aMimeType);
-        return IntentHelper.getHandlersForIntent(intent);
+        return getHandlersForIntent(intent);
     }
 
     @WrapForJNI(stubName = "GetHandlersForURLWrapper")
     static String[] getHandlersForURL(String aURL, String aAction) {
         // aURL may contain the whole URL or just the protocol
         Uri uri = aURL.indexOf(':') >= 0 ? Uri.parse(aURL) : new Uri.Builder().scheme(aURL).build();
 
-        Intent intent = IntentHelper.getOpenURIIntent(getApplicationContext(), uri.toString(), "",
+        Intent intent = getOpenURIIntent(getApplicationContext(), uri.toString(), "",
             TextUtils.isEmpty(aAction) ? Intent.ACTION_VIEW : aAction, "");
 
-        return IntentHelper.getHandlersForIntent(intent);
+        return getHandlersForIntent(intent);
     }
 
     @WrapForJNI(stubName = "GetHWEncoderCapability")
     static boolean getHWEncoderCapability() {
       return HardwareCodecCapabilityUtils.getHWEncoderCapability();
     }
 
     @WrapForJNI(stubName = "GetHWDecoderCapability")
@@ -872,16 +878,57 @@ public class GeckoAppShell
             if (ri.activityInfo.exported) {
                 list.add(ri);
             }
         }
 
         return list;
     }
 
+    static boolean hasHandlersForIntent(Intent intent) {
+        try {
+            return !queryIntentActivities(intent).isEmpty();
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "Exception in GeckoAppShell.hasHandlersForIntent");
+            return false;
+        }
+    }
+
+    static String[] getHandlersForIntent(Intent intent) {
+        final PackageManager pm = getApplicationContext().getPackageManager();
+        try {
+            final List<ResolveInfo> list = queryIntentActivities(intent);
+
+            int numAttr = 4;
+            final String[] ret = new String[list.size() * numAttr];
+            for (int i = 0; i < list.size(); i++) {
+                ResolveInfo resolveInfo = list.get(i);
+                ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
+                if (resolveInfo.isDefault)
+                    ret[i * numAttr + 1] = "default";
+                else
+                    ret[i * numAttr + 1] = "";
+                ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
+                ret[i * numAttr + 3] = resolveInfo.activityInfo.name;
+            }
+            return ret;
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "Exception in GeckoAppShell.getHandlersForIntent");
+            return new String[0];
+        }
+    }
+
+    static Intent getIntentForActionString(String aAction) {
+        // Default to the view action if no other action as been specified.
+        if (TextUtils.isEmpty(aAction)) {
+            return new Intent(Intent.ACTION_VIEW);
+        }
+        return new Intent(aAction);
+    }
+
     @WrapForJNI(stubName = "GetExtensionFromMimeTypeWrapper")
     static String getExtensionFromMimeType(String aMimeType) {
         return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
     }
 
     @WrapForJNI(stubName = "GetMimeTypeFromExtensionsWrapper")
     static String getMimeTypeFromExtensions(String aFileExt) {
         StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
@@ -923,19 +970,287 @@ public class GeckoAppShell
 
     @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);
+    }
 
-        // Default to showing prompt in private browsing to be safe.
-        return IntentHelper.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.
+     * @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.
+     */
+    public static boolean openUriExternal(String targetURI,
+                                          String mimeType,
+                                          String packageName,
+                                          String className,
+                                          String action,
+                                          String title,
+                                          final boolean showPromptInPrivateBrowsing) {
+        final GeckoInterface gi = getGeckoInterface();
+        final Context activityContext = gi != null ? gi.getActivity() : null;
+        final Context context = activityContext != null ? activityContext : getApplicationContext();
+        final Intent intent = getOpenURIIntent(context, targetURI,
+                                               mimeType, action, title);
+
+        if (intent == null) {
+            return false;
+        }
+
+        if (!TextUtils.isEmpty(className)) {
+            if (!TextUtils.isEmpty(packageName)) {
+                intent.setClassName(packageName, className);
+            } else {
+                // Default to using the fennec app context.
+                intent.setClassName(context, className);
+            }
+        }
+
+        if (!showPromptInPrivateBrowsing || activityContext == null) {
+            if (activityContext == null) {
+                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            }
+            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) activityContext;
+            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.
+     *
+     * @param u the <code>Uri</code> to normalize.
+     * @return a <code>Uri</code>, which might be <code>u</code>.
+     */
+    static Uri normalizeUriScheme(final Uri u) {
+        final String scheme = u.getScheme();
+        final String lower  = scheme.toLowerCase(Locale.US);
+        if (lower.equals(scheme)) {
+            return u;
+        }
+
+        // Otherwise, return a new URI with a normalized scheme.
+        return u.buildUpon().scheme(lower).build();
+    }
+
+    /**
+     * Given a URI, a MIME type, and a title,
+     * produce a share intent which can be used to query all activities
+     * than can open the specified URI.
+     *
+     * @param context a <code>Context</code> instance.
+     * @param targetURI the string spec of the URI to open.
+     * @param mimeType an optional MIME type string.
+     * @param title the title to use in <code>ACTION_SEND</code> intents.
+     * @return an <code>Intent</code>, or <code>null</code> if none could be
+     *         produced.
+     */
+    public static Intent getShareIntent(final Context context,
+                                        final String targetURI,
+                                        final String mimeType,
+                                        final String title) {
+        Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND);
+        shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI);
+        shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
+        shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
+
+        // Note that EXTRA_TITLE is intended to be used for share dialog
+        // titles. Common usage (e.g., Pocket) suggests that it's sometimes
+        // interpreted as an alternate to EXTRA_SUBJECT, so we include it.
+        shareIntent.putExtra(Intent.EXTRA_TITLE, title);
+
+        if (mimeType != null && mimeType.length() > 0) {
+            shareIntent.setType(mimeType);
+        }
+
+        return shareIntent;
+    }
+
+    /**
+     * Given a URI, a MIME type, an Android intent "action", and a title,
+     * produce an intent which can be used to start an activity to open
+     * the specified URI.
+     *
+     * @param context a <code>Context</code> instance.
+     * @param targetURI the string spec of the URI to open.
+     * @param mimeType an optional MIME type string.
+     * @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 an <code>Intent</code>, or <code>null</code> if none could be
+     *         produced.
+     */
+    static Intent getOpenURIIntent(final Context context,
+                                   final String targetURI,
+                                   final String mimeType,
+                                   final String action,
+                                   final String title) {
+
+        // The resultant chooser can return non-exported activities in 4.1 and earlier.
+        // https://code.google.com/p/android/issues/detail?id=29535
+        final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
+
+        if (intent != null) {
+            // Some applications use this field to return to the same browser after processing the
+            // Intent. While there is some danger (e.g. denial of service), other major browsers already
+            // use it and so it's the norm.
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME);
+        }
+
+        return intent;
+    }
+
+    private static Intent getOpenURIIntentInner(final Context context,  final String targetURI,
+            final String mimeType, final String action, final String title) {
+
+        if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
+            Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
+            return Intent.createChooser(shareIntent,
+                                        context.getResources().getString(R.string.share_title)); 
+        }
+
+        Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
+        if (!TextUtils.isEmpty(mimeType)) {
+            Intent intent = getIntentForActionString(action);
+            intent.setDataAndType(uri, mimeType);
+            return intent;
+        }
+
+        if (!isUriSafeForScheme(uri)) {
+            return null;
+        }
+
+        final String scheme = uri.getScheme();
+        if ("intent".equals(scheme) || "android-app".equals(scheme)) {
+            final Intent intent;
+            try {
+                intent = Intent.parseUri(targetURI, 0);
+            } catch (final URISyntaxException e) {
+                Log.e(LOGTAG, "Unable to parse URI - " + e);
+                return null;
+            }
+
+            // Only open applications which can accept arbitrary data from a browser.
+            intent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+            // Prevent site from explicitly opening our internal activities, which can leak data.
+            intent.setComponent(null);
+            nullIntentSelector(intent);
+
+            return intent;
+        }
+
+        // Compute our most likely intent, then check to see if there are any
+        // custom handlers that would apply.
+        // Start with the original URI. If we end up modifying it, we'll
+        // overwrite it.
+        final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
+        final Intent intent = getIntentForActionString(action);
+        intent.setData(uri);
+
+        if ("file".equals(scheme)) {
+            // Only set explicit mimeTypes on file://.
+            final String mimeType2 = getMimeTypeFromExtension(extension);
+            intent.setType(mimeType2);
+            return intent;
+        }
+
+        // Have a special handling for SMS based schemes, as the query parameters
+        // are not extracted from the URI automatically.
+        if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
+            return intent;
+        }
+
+        final String query = uri.getEncodedQuery();
+        if (TextUtils.isEmpty(query)) {
+            return intent;
+        }
+
+        // It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
+        // but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
+        String currentUri = uri.toString();
+        String correctlyFormattedDataURIScheme = scheme + "://";
+        if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
+            uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
+        }
+
+        final String[] fields = query.split("&");
+        boolean shouldUpdateIntent = false;
+        String resultQuery = "";
+        for (String field : fields) {
+            if (field.startsWith("body=")) {
+                final String body = Uri.decode(field.substring(5));
+                intent.putExtra("sms_body", body);
+                shouldUpdateIntent = true;
+            } else if (field.startsWith("subject=")) {
+                final String subject = Uri.decode(field.substring(8));
+                intent.putExtra("subject", subject);
+                shouldUpdateIntent = true;
+            } else if (field.startsWith("cc=")) {
+                final String ccNumber = Uri.decode(field.substring(3));
+                String phoneNumber = uri.getAuthority();
+                if (phoneNumber != null) {
+                    uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
+                }
+                shouldUpdateIntent = true;
+            } else {
+                resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
+            }
+        }
+
+        if (!shouldUpdateIntent) {
+            // No need to rewrite the URI, then.
+            return intent;
+        }
+
+        // Form a new URI without the extracted fields in the query part, and
+        // push that into the new Intent.
+        final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
+        final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
+        intent.setData(pruned);
+
+        return intent;
+    }
+
+    // We create a separate method to better encapsulate the @TargetApi use.
+    @TargetApi(15)
+    private static void nullIntentSelector(final Intent intent) {
+        if (!Versions.feature15Plus) {
+            return;
+        }
+
+        intent.setSelector(null);
     }
 
     /**
      * Only called from GeckoApp.
      */
     public static void setNotificationClient(NotificationClient client) {
         if (notificationClient == null) {
             notificationClient = client;
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -1,41 +1,34 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
 
-import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.JSONUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.WebActivityMapper;
 import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import android.annotation.TargetApi;
-import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.net.Uri;
-import android.provider.Browser;
 import android.support.annotation.Nullable;
 import android.support.v4.app.FragmentActivity;
 import android.text.TextUtils;
 import android.util.Log;
-import android.webkit.MimeTypeMap;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
@@ -85,326 +78,16 @@ public final class IntentHelper implemen
     public static void destroy() {
         if (instance != null) {
             EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) instance, EVENTS);
             EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) instance, NATIVE_EVENTS);
             instance = null;
         }
     }
 
-    /**
-     * 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.
-     * @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.
-     */
-    public static boolean openUriExternal(String targetURI,
-                                          String mimeType,
-                                          String packageName,
-                                          String className,
-                                          String action,
-                                          String title,
-                                          final boolean showPromptInPrivateBrowsing) {
-        final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
-        final Context activityContext = gi != null ? gi.getActivity() : null;
-        final Context context = activityContext != null ? activityContext : GeckoAppShell.getApplicationContext();
-        final Intent intent = getOpenURIIntent(context, targetURI,
-                                               mimeType, action, title);
-
-        if (intent == null) {
-            return false;
-        }
-
-        if (!TextUtils.isEmpty(className)) {
-            if (!TextUtils.isEmpty(packageName)) {
-                intent.setClassName(packageName, className);
-            } else {
-                // Default to using the fennec app context.
-                intent.setClassName(context, className);
-            }
-        }
-
-        if (!showPromptInPrivateBrowsing || activityContext == null) {
-            if (activityContext == null) {
-                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            }
-            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) activityContext;
-            return ExternalIntentDuringPrivateBrowsingPromptFragment.showDialogOrAndroidChooser(
-                    context, fragmentActivity.getSupportFragmentManager(), intent);
-        }
-    }
-
-    public static boolean hasHandlersForIntent(Intent intent) {
-        try {
-            return !GeckoAppShell.queryIntentActivities(intent).isEmpty();
-        } catch (Exception ex) {
-            Log.e(LOGTAG, "Exception in hasHandlersForIntent");
-            return false;
-        }
-    }
-
-    public static String[] getHandlersForIntent(Intent intent) {
-        final PackageManager pm = GeckoAppShell.getApplicationContext().getPackageManager();
-        try {
-            final List<ResolveInfo> list = GeckoAppShell.queryIntentActivities(intent);
-
-            int numAttr = 4;
-            final String[] ret = new String[list.size() * numAttr];
-            for (int i = 0; i < list.size(); i++) {
-                ResolveInfo resolveInfo = list.get(i);
-                ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
-                if (resolveInfo.isDefault)
-                    ret[i * numAttr + 1] = "default";
-                else
-                    ret[i * numAttr + 1] = "";
-                ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
-                ret[i * numAttr + 3] = resolveInfo.activityInfo.name;
-            }
-            return ret;
-        } catch (Exception ex) {
-            Log.e(LOGTAG, "Exception in getHandlersForIntent");
-            return new String[0];
-        }
-    }
-
-    public static Intent getIntentForActionString(String aAction) {
-        // Default to the view action if no other action as been specified.
-        if (TextUtils.isEmpty(aAction)) {
-            return new Intent(Intent.ACTION_VIEW);
-        }
-        return new Intent(aAction);
-    }
-
-    /**
-     * Given a URI, a MIME type, and a title,
-     * produce a share intent which can be used to query all activities
-     * than can open the specified URI.
-     *
-     * @param context a <code>Context</code> instance.
-     * @param targetURI the string spec of the URI to open.
-     * @param mimeType an optional MIME type string.
-     * @param title the title to use in <code>ACTION_SEND</code> intents.
-     * @return an <code>Intent</code>, or <code>null</code> if none could be
-     *         produced.
-     */
-    public static Intent getShareIntent(final Context context,
-                                        final String targetURI,
-                                        final String mimeType,
-                                        final String title) {
-        Intent shareIntent = getIntentForActionString(Intent.ACTION_SEND);
-        shareIntent.putExtra(Intent.EXTRA_TEXT, targetURI);
-        shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
-        shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
-
-        // Note that EXTRA_TITLE is intended to be used for share dialog
-        // titles. Common usage (e.g., Pocket) suggests that it's sometimes
-        // interpreted as an alternate to EXTRA_SUBJECT, so we include it.
-        shareIntent.putExtra(Intent.EXTRA_TITLE, title);
-
-        if (mimeType != null && mimeType.length() > 0) {
-            shareIntent.setType(mimeType);
-        }
-
-        return shareIntent;
-    }
-
-    /**
-     * Given a URI, a MIME type, an Android intent "action", and a title,
-     * produce an intent which can be used to start an activity to open
-     * the specified URI.
-     *
-     * @param context a <code>Context</code> instance.
-     * @param targetURI the string spec of the URI to open.
-     * @param mimeType an optional MIME type string.
-     * @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 an <code>Intent</code>, or <code>null</code> if none could be
-     *         produced.
-     */
-    static Intent getOpenURIIntent(final Context context,
-                                   final String targetURI,
-                                   final String mimeType,
-                                   final String action,
-                                   final String title) {
-
-        // The resultant chooser can return non-exported activities in 4.1 and earlier.
-        // https://code.google.com/p/android/issues/detail?id=29535
-        final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
-
-        if (intent != null) {
-            // Some applications use this field to return to the same browser after processing the
-            // Intent. While there is some danger (e.g. denial of service), other major browsers already
-            // use it and so it's the norm.
-            intent.putExtra(Browser.EXTRA_APPLICATION_ID, AppConstants.ANDROID_PACKAGE_NAME);
-        }
-
-        return intent;
-    }
-
-    private static Intent getOpenURIIntentInner(final Context context, final String targetURI,
-                                                final String mimeType, final String action, final String title) {
-
-        if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
-            Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
-            return Intent.createChooser(shareIntent,
-                                        context.getResources().getString(R.string.share_title));
-        }
-
-        Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
-        if (!TextUtils.isEmpty(mimeType)) {
-            Intent intent = getIntentForActionString(action);
-            intent.setDataAndType(uri, mimeType);
-            return intent;
-        }
-
-        if (!GeckoAppShell.isUriSafeForScheme(uri)) {
-            return null;
-        }
-
-        final String scheme = uri.getScheme();
-        if ("intent".equals(scheme) || "android-app".equals(scheme)) {
-            final Intent intent;
-            try {
-                intent = Intent.parseUri(targetURI, 0);
-            } catch (final URISyntaxException e) {
-                Log.e(LOGTAG, "Unable to parse URI - " + e);
-                return null;
-            }
-
-            // Only open applications which can accept arbitrary data from a browser.
-            intent.addCategory(Intent.CATEGORY_BROWSABLE);
-
-            // Prevent site from explicitly opening our internal activities, which can leak data.
-            intent.setComponent(null);
-            nullIntentSelector(intent);
-
-            return intent;
-        }
-
-        // Compute our most likely intent, then check to see if there are any
-        // custom handlers that would apply.
-        // Start with the original URI. If we end up modifying it, we'll
-        // overwrite it.
-        final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
-        final Intent intent = getIntentForActionString(action);
-        intent.setData(uri);
-
-        if ("file".equals(scheme)) {
-            // Only set explicit mimeTypes on file://.
-            final String mimeType2 = GeckoAppShell.getMimeTypeFromExtension(extension);
-            intent.setType(mimeType2);
-            return intent;
-        }
-
-        // Have a special handling for SMS based schemes, as the query parameters
-        // are not extracted from the URI automatically.
-        if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
-            return intent;
-        }
-
-        final String query = uri.getEncodedQuery();
-        if (TextUtils.isEmpty(query)) {
-            return intent;
-        }
-
-        // It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
-        // but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
-        String currentUri = uri.toString();
-        String correctlyFormattedDataURIScheme = scheme + "://";
-        if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
-            uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
-        }
-
-        final String[] fields = query.split("&");
-        boolean shouldUpdateIntent = false;
-        String resultQuery = "";
-        for (String field : fields) {
-            if (field.startsWith("body=")) {
-                final String body = Uri.decode(field.substring(5));
-                intent.putExtra("sms_body", body);
-                shouldUpdateIntent = true;
-            } else if (field.startsWith("subject=")) {
-                final String subject = Uri.decode(field.substring(8));
-                intent.putExtra("subject", subject);
-                shouldUpdateIntent = true;
-            } else if (field.startsWith("cc=")) {
-                final String ccNumber = Uri.decode(field.substring(3));
-                String phoneNumber = uri.getAuthority();
-                if (phoneNumber != null) {
-                    uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
-                }
-                shouldUpdateIntent = true;
-            } else {
-                resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
-            }
-        }
-
-        if (!shouldUpdateIntent) {
-            // No need to rewrite the URI, then.
-            return intent;
-        }
-
-        // Form a new URI without the extracted fields in the query part, and
-        // push that into the new Intent.
-        final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
-        final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
-        intent.setData(pruned);
-
-        return intent;
-    }
-
-    // We create a separate method to better encapsulate the @TargetApi use.
-    @TargetApi(15)
-    private static void nullIntentSelector(final Intent intent) {
-        if (!AppConstants.Versions.feature15Plus) {
-            return;
-        }
-
-        intent.setSelector(null);
-    }
-
-    /**
-     * 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.
-     *
-     * @param u the <code>Uri</code> to normalize.
-     * @return a <code>Uri</code>, which might be <code>u</code>.
-     */
-    private static Uri normalizeUriScheme(final Uri u) {
-        final String scheme = u.getScheme();
-        final String lower  = scheme.toLowerCase(Locale.US);
-        if (lower.equals(scheme)) {
-            return u;
-        }
-
-        // Otherwise, return a new URI with a normalized scheme.
-        return u.buildUpon().scheme(lower).build();
-    }
-
     @Override
     public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
         if (event.equals("Intent:OpenNoHandler")) {
             openNoHandler(message, callback);
         }
     }
 
     @Override
@@ -420,39 +103,39 @@ public final class IntentHelper implemen
                 openWebActivity(message);
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     private void getHandlers(JSONObject message) throws JSONException {
-        final Intent intent = getOpenURIIntent(activity,
+        final Intent intent = GeckoAppShell.getOpenURIIntent(activity,
                                                              message.optString("url"),
                                                              message.optString("mime"),
                                                              message.optString("action"),
                                                              message.optString("title"));
-        final List<String> appList = Arrays.asList(getHandlersForIntent(intent));
+        final List<String> appList = Arrays.asList(GeckoAppShell.getHandlersForIntent(intent));
 
         final JSONObject response = new JSONObject();
         response.put("apps", new JSONArray(appList));
         EventDispatcher.sendResponse(message, response);
     }
 
     private void open(JSONObject message) throws JSONException {
-        openUriExternal(message.optString("url"),
+        GeckoAppShell.openUriExternal(message.optString("url"),
                                       message.optString("mime"),
                                       message.optString("packageName"),
                                       message.optString("className"),
                                       message.optString("action"),
                                       message.optString("title"), false);
     }
 
     private void openForResult(final JSONObject message) throws JSONException {
-        Intent intent = getOpenURIIntent(activity,
+        Intent intent = GeckoAppShell.getOpenURIIntent(activity,
                                                        message.optString("url"),
                                                        message.optString("mime"),
                                                        message.optString("action"),
                                                        message.optString("title"));
         intent.setClassName(message.optString("packageName"), message.optString("className"));
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
 
         final ResultHandler handler = new ResultHandler(message);
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko.home;
 
 import java.util.EnumSet;
 
 import org.mozilla.gecko.EditBookmarkDialog;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.Restrictions;
 import org.mozilla.gecko.SnackbarHelper;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
@@ -206,17 +205,17 @@ public abstract class HomeFragment exten
             return true;
         }
 
         if (itemId == R.id.home_share) {
             if (info.url == null) {
                 Log.e(LOGTAG, "Can't share because URL is null");
                 return false;
             } else {
-                IntentHelper.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
+                GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
                                               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, "home_contextmenu");
                 return true;
             }
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
@@ -1,21 +1,22 @@
 package org.mozilla.gecko.prompts;
 
-import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
+import org.json.JSONException;
 
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
+import android.util.Log;
 
 import java.util.List;
 import java.util.ArrayList;
 
 // This class should die and be replaced with normal menu items
 public class PromptListItem {
     private static final String LOGTAG = "GeckoPromptListItem";
     public final String label;
@@ -41,17 +42,17 @@ public class PromptListItem {
 
         JSONObject obj = aObject.optJSONObject("showAsActions");
         if (obj != null) {
             showAsActions = true;
             String uri = obj.isNull("uri") ? "" : obj.optString("uri");
             String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE :
                                                obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
 
-            mIntent = IntentHelper.getShareIntent(context, uri, type, "");
+            mIntent = GeckoAppShell.getShareIntent(context, uri, type, "");
             isParent = true;
         } else {
             mIntent = null;
             showAsActions = false;
             // Support both "isParent" (backwards compat for older consumers), and "menu" for the new Tabbed prompt ui.
             isParent = aObject.optBoolean("isParent") || aObject.optBoolean("menu");
         }