Upstream: Launch WebAPK without showing intent picker when user taps link in WebAPK scope

This CL surpresses the intent picker when:
- User navigates via a link
AND
- The URL falls in the scope of a WebAPK
AND
- The WebAPK is the only specialized handler for the URL

BUG=609122
TEST=ExternalNavigationHandlerTest.*

Review-Url: https://codereview.chromium.org/2035183002
Cr-Commit-Position: refs/heads/master@{#398946}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegate.java
index cbf9cd0..6a4cc99c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegate.java
@@ -35,6 +35,19 @@
     boolean isSpecializedHandlerAvailable(List<ResolveInfo> intent);
 
     /**
+     * Returns the number of specialized intent handlers in {@params infos}. Specialized intent
+     * handlers are intent handlers which handle only a few URLs (e.g. google maps or youtube).
+     */
+    int countSpecializedHandlers(List<ResolveInfo> infos);
+
+    /**
+     * Returns the package name of the first valid WebAPK in {@link infos}.
+     * @param infos ResolveInfos to search.
+     * @return The package name of the first valid WebAPK. Null if no valid WebAPK was found.
+     */
+    String findValidWebApkPackageName(List<ResolveInfo> infos);
+
+    /**
      * Get the name of the package of the currently running activity so that incoming intents
      * can be identified as originating from this activity.
      */
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImpl.java
index f6946ec..a03a389 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImpl.java
@@ -12,7 +12,6 @@
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
@@ -45,6 +44,7 @@
 import org.chromium.ui.base.PageTransition;
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.base.WindowAndroid.PermissionCallback;
+import org.chromium.webapk.lib.client.WebApkValidator;
 
 import java.util.List;
 
@@ -227,31 +227,41 @@
 
     @Override
     public boolean isSpecializedHandlerAvailable(List<ResolveInfo> infos) {
-        return isPackageSpecializedHandler(infos, null);
+        return countSpecializedHandlers(infos) > 0;
     }
 
-    static boolean isPackageSpecializedHandler(List<ResolveInfo> handlers,
-            String packageName) {
-        if (handlers == null || handlers.size() == 0) return false;
-        for (ResolveInfo resolveInfo : handlers) {
-            IntentFilter filter = resolveInfo.filter;
+    @Override
+    public int countSpecializedHandlers(List<ResolveInfo> infos) {
+        return countSpecializedHandlersWithFilter(infos, null);
+    }
+
+    static int countSpecializedHandlersWithFilter(
+            List<ResolveInfo> infos, String filterPackageName) {
+        if (infos == null) {
+            return 0;
+        }
+
+        int count = 0;
+        for (ResolveInfo info : infos) {
+            IntentFilter filter = info.filter;
             if (filter == null) {
-                // No intent filter matches this intent?
-                // Error on the side of staying in the browser, ignore
+                // Error on the side of classifying ResolveInfo as generic.
                 continue;
             }
             if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) {
-                // Generic handler, skip
+                // Don't count generic handlers.
                 continue;
             }
-            if (TextUtils.isEmpty(packageName)) return true;
-            ActivityInfo activityInfo = resolveInfo.activityInfo;
-            if (activityInfo == null) continue;
-            if (!activityInfo.packageName.equals(packageName)) continue;
-            return true;
-        }
 
-        return false;
+            if (!TextUtils.isEmpty(filterPackageName)
+                    && (info.activityInfo == null
+                               || !info.activityInfo.packageName.equals(filterPackageName))) {
+                continue;
+            }
+
+            ++count;
+        }
+        return count;
     }
 
     /**
@@ -268,7 +278,7 @@
         try {
             List<ResolveInfo> handlers = context.getPackageManager().queryIntentActivities(
                     intent, PackageManager.GET_RESOLVED_FILTER);
-            return isPackageSpecializedHandler(handlers, packageName);
+            return countSpecializedHandlersWithFilter(handlers, packageName) > 0;
         } catch (RuntimeException e) {
             logTransactionTooLargeOrRethrow(e, intent);
         }
@@ -276,6 +286,14 @@
     }
 
     @Override
+    public String findValidWebApkPackageName(List<ResolveInfo> infos) {
+        String webApkPackageName = WebApkValidator.findWebApkPackage(infos);
+        return WebApkValidator.isValidWebApk(mApplicationContext, webApkPackageName)
+                ? webApkPackageName
+                : null;
+    }
+
+    @Override
     public String getPackageName() {
         return mApplicationContext.getPackageName();
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandler.java
index 62e7b26..8706863 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandler.java
@@ -347,7 +347,7 @@
         // startActivityIfNeeded or startActivity.
         if (!isExternalProtocol) {
             if (!mDelegate.isSpecializedHandlerAvailable(resolvingInfos)) {
-                if (params.isWebApk()) {
+                if (params.webApkPackageName() != null) {
                     intent.setPackage(mDelegate.getPackageName());
                     mDelegate.startActivity(intent);
                     return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
@@ -412,6 +412,17 @@
                     IntentWithGesturesHandler.getInstance().onNewIntentWithGesture(intent);
                 }
 
+                if (CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_WEBAPK)) {
+                    // If the only specialized intent handler is a WebAPK, set the intent's package
+                    // to launch the WebAPK without showing the intent picker.
+                    String targetWebApkPackageName =
+                            mDelegate.findValidWebApkPackageName(resolvingInfos);
+                    if (targetWebApkPackageName != null
+                            && mDelegate.countSpecializedHandlers(resolvingInfos) == 1) {
+                        intent.setPackage(targetWebApkPackageName);
+                    }
+                }
+
                 if (mDelegate.startActivityIfNeeded(intent)) {
                     return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
                 } else {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationParams.java b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationParams.java
index 403bfbce..7d6192b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationParams.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/externalnav/ExternalNavigationParams.java
@@ -43,8 +43,11 @@
     /** Whether this navigation happens in main frame. */
     private final boolean mIsMainFrame;
 
-    /** Whether this navigation happens in a WebAPK. */
-    private final boolean mIsWebApk;
+    /**
+     * The package name of the WebAPK that the navigation happens in. Null if the navigation is not
+     * happening in a WebAPK.
+     */
+    private final String mWebApkPackageName;
 
     /** Whether this navigation is launched by user gesture. */
     private final boolean mHasUserGesture;
@@ -58,7 +61,7 @@
     private ExternalNavigationParams(String url, boolean isIncognito, String referrerUrl,
             int pageTransition, boolean isRedirect, boolean appMustBeInForeground,
             TabRedirectHandler redirectHandler, Tab tab, boolean openInNewTab,
-            boolean isBackgroundTabNavigation, boolean isMainFrame, boolean isWebApk,
+            boolean isBackgroundTabNavigation, boolean isMainFrame, String webApkPackageName,
             boolean hasUserGesture,
             boolean shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent) {
         mUrl = url;
@@ -72,7 +75,7 @@
         mOpenInNewTab = openInNewTab;
         mIsBackgroundTabNavigation = isBackgroundTabNavigation;
         mIsMainFrame = isMainFrame;
-        mIsWebApk = isWebApk;
+        mWebApkPackageName = webApkPackageName;
         mHasUserGesture = hasUserGesture;
         mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent =
                 shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent;
@@ -136,9 +139,12 @@
         return mIsMainFrame;
     }
 
-    /** @return Whether this navigation happens in a WebAPK. */
-    public boolean isWebApk() {
-        return mIsWebApk;
+    /**
+     * @return Package name of the WebAPK that the navigation happens in. Null if the navigation is
+     *         not happening in a WebAPK.
+     */
+    public String webApkPackageName() {
+        return mWebApkPackageName;
     }
 
     /** @return Whether this navigation is launched by user gesture. */
@@ -188,8 +194,11 @@
         /** Whether this navigation happens in main frame. */
         private boolean mIsMainFrame;
 
-        /** Whether this navigation happens in a WebAPK. */
-        private boolean mIsWebApk;
+        /**
+         * The package name of the WebAPK that the navigation happens in. Null if the navigation is
+         * not happening in a WebAPK.
+         */
+        private String mWebApkPackageName;
 
         /** Whether this navigation is launched by user gesture. */
         private boolean mHasUserGesture;
@@ -250,9 +259,12 @@
             return this;
         }
 
-        /** Sets whether this navigation happens in a WebAPK. */
-        public Builder setIsWebApk(boolean v) {
-            mIsWebApk = v;
+        /**
+         * Sets the package name of the WebAPK that the navigation happens in. Null if the
+         * navigation is not happening in a WebAPK.
+         */
+        public Builder setWebApkPackageName(String v) {
+            mWebApkPackageName = v;
             return this;
         }
 
@@ -274,7 +286,7 @@
         public ExternalNavigationParams build() {
             return new ExternalNavigationParams(mUrl, mIsIncognito, mReferrerUrl, mPageTransition,
                     mIsRedirect, mApplicationMustBeInForeground, mRedirectHandler, mTab,
-                    mOpenInNewTab, mIsBackgroundTabNavigation, mIsMainFrame, mIsWebApk,
+                    mOpenInNewTab, mIsBackgroundTabNavigation, mIsMainFrame, mWebApkPackageName,
                     mHasUserGesture, mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent);
         }
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkActivity.java
index d37fa440..42b8d2e8 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkActivity.java
@@ -65,7 +65,7 @@
                         ExternalNavigationParams.Builder builder =
                                 super.buildExternalNavigationParams(
                                         navigationParams, tabRedirectHandler, shouldCloseTab);
-                        builder.setIsWebApk(true);
+                        builder.setWebApkPackageName(getWebApkPackageName());
                         return builder;
                     }
                 };
@@ -83,9 +83,15 @@
 
     public void onStop() {
         super.onStop();
-        String packageName = getWebappInfo().webApkPackageName();
         WebApkServiceConnectionManager.getInstance().disconnect(
-                ContextUtils.getApplicationContext(), packageName);
+                ContextUtils.getApplicationContext(), getWebApkPackageName());
+    }
+
+    /**
+     * Returns the WebAPK's package name.
+     */
+    private String getWebApkPackageName() {
+        return getWebappInfo().webApkPackageName();
     }
 
     @Override
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImplTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImplTest.java
index 9dc032e..34e320c 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImplTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationDelegateImplTest.java
@@ -34,8 +34,8 @@
     public void testIsPackageSpecializedHandler_NoResolveInfo() {
         String packageName = "";
         List<ResolveInfo> resolveInfos = new ArrayList<ResolveInfo>();
-        assertFalse(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(0, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
@@ -44,8 +44,8 @@
         ResolveInfo info = new ResolveInfo();
         info.filter = new IntentFilter();
         List<ResolveInfo> resolveInfos = makeResolveInfos(info);
-        assertFalse(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(0, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
@@ -55,8 +55,8 @@
         info.filter = new IntentFilter();
         info.filter.addDataPath("somepath", 2);
         List<ResolveInfo> resolveInfos = makeResolveInfos(info);
-        assertTrue(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(1, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
@@ -66,8 +66,8 @@
         info.filter = new IntentFilter();
         info.filter.addDataAuthority("http://www.google.com", "80");
         List<ResolveInfo> resolveInfos = makeResolveInfos(info);
-        assertTrue(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(1, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
@@ -79,8 +79,8 @@
         info.activityInfo = new ActivityInfo();
         info.activityInfo.packageName = packageName;
         List<ResolveInfo> resolveInfos = makeResolveInfos(info);
-        assertTrue(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(1, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
@@ -92,8 +92,8 @@
         info.activityInfo = new ActivityInfo();
         info.activityInfo.packageName = "com.foo.bar";
         List<ResolveInfo> resolveInfos = makeResolveInfos(info);
-        assertFalse(ExternalNavigationDelegateImpl
-                .isPackageSpecializedHandler(resolveInfos, packageName));
+        assertEquals(0, ExternalNavigationDelegateImpl.countSpecializedHandlersWithFilter(
+                                resolveInfos, packageName));
     }
 
     @SmallTest
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandlerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandlerTest.java
index 87a6158..0640deb 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandlerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/ExternalNavigationHandlerTest.java
@@ -20,12 +20,14 @@
 
 import org.chromium.base.CommandLine;
 import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.chrome.browser.ChromeSwitches;
 import org.chromium.chrome.browser.IntentHandler;
 import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler.OverrideUrlLoadingResult;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabRedirectHandler;
 import org.chromium.chrome.browser.util.FeatureUtilities;
 import org.chromium.ui.base.PageTransition;
+import org.chromium.webapk.lib.common.WebApkConstants;
 
 import java.net.URISyntaxException;
 import java.util.ArrayList;
@@ -40,9 +42,10 @@
     private static final int IGNORE = 0x0;
     private static final int START_INCOGNITO = 0x1;
     private static final int START_CHROME = 0x2;
-    private static final int START_FILE = 0x4;
-    private static final int START_OTHER_ACTIVITY = 0x8;
-    private static final int INTENT_SANITIZATION_EXCEPTION = 0x10;
+    private static final int START_WEBAPK = 0x4;
+    private static final int START_FILE = 0x8;
+    private static final int START_OTHER_ACTIVITY = 0x10;
+    private static final int INTENT_SANITIZATION_EXCEPTION = 0x20;
 
     private static final String SEARCH_RESULT_URL_FOR_TOM_HANKS =
             "https://www.google.com/search?q=tom+hanks";
@@ -73,6 +76,19 @@
     private static final String TEXT_APP_1_PACKAGE_NAME = "text_app_1";
     private static final String TEXT_APP_2_PACKAGE_NAME = "text_app_2";
 
+    private static final String WEBAPK_SCOPE = "https://www.template.com";
+    private static final String WEBAPK_PACKAGE_NAME = "org.chromium.webapk.template";
+
+    private static final String WEBAPK_WITH_NATIVE_APP_SCOPE =
+            "https://www.webapk.with.native.com";
+    private static final String WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME =
+            "org.chromium.webapk.with.native";
+    private static final String NATIVE_APP_PACKAGE_NAME = "com.webapk.with.native.android";
+
+    private static final String COUNTERFEIT_WEBAPK_SCOPE = "http://www.counterfeit.webapk.com";
+    private static final String COUNTERFEIT_WEBAPK_PACKAGE_NAME =
+            "org.chromium.webapk.counterfeit";
+
     private final TestExternalNavigationDelegate mDelegate;
     private ExternalNavigationHandler mUrlHandler;
 
@@ -750,16 +766,89 @@
     }
 
     /**
-     * Test that tapping on a link which is outside of the referrer Web APK's scope brings the
+     * Test that tapping on a link which is outside of the referrer WebAPK's scope brings the
      * user back to Chrome.
      */
     @SmallTest
     public void testLeaveWebApk_LinkOutOfScope() {
         checkUrl(SEARCH_RESULT_URL_FOR_TOM_HANKS)
-                .withIsWebApk(true)
+                .withWebApkPackageName(WEBAPK_PACKAGE_NAME)
                 .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT, START_CHROME);
     }
 
+    /**
+     * Test that tapping a link which falls solely into the scope of a WebAPK does not bypass the
+     * intent picker if WebAPKs are disabled in the command line.
+     */
+    @SmallTest
+    public void testLaunchWebApk_WebApkDisabledCommandLine() {
+        checkUrl(WEBAPK_SCOPE)
+                .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT,
+                        START_OTHER_ACTIVITY);
+    }
+
+    /**
+     * Test that tapping a link which falls solely in the scope of a WebAPK launches a WebAPK
+     * without showing the intent picker if WebAPKs are enabled in the command line.
+     */
+    @SmallTest
+    public void testLaunchWebApk_BypassIntentPicker() {
+        CommandLine.getInstance().appendSwitch(ChromeSwitches.ENABLE_WEBAPK);
+        checkUrl(WEBAPK_SCOPE)
+                .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT, START_WEBAPK);
+    }
+
+    /**
+     * Test that tapping a link which falls in the scope of multiple intent handlers, one of which
+     * is a WebAPK, shows the intent picker.
+     */
+    @SmallTest
+    public void testLaunchWebApk_ShowIntentPickerMultipleIntentHandlers() {
+        CommandLine.getInstance().appendSwitch(ChromeSwitches.ENABLE_WEBAPK);
+        checkUrl(WEBAPK_WITH_NATIVE_APP_SCOPE)
+                .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT,
+                        START_OTHER_ACTIVITY);
+    }
+
+    /**
+     * Test that tapping a link which falls solely into the scope of a different WebAPK launches a
+     * WebAPK without showing the intent picker.
+     */
+    @SmallTest
+    public void testLaunchWebApk_BypassIntentPickerFromAnotherWebApk() {
+        CommandLine.getInstance().appendSwitch(ChromeSwitches.ENABLE_WEBAPK);
+        checkUrl(WEBAPK_SCOPE)
+                .withReferrer(WEBAPK_WITH_NATIVE_APP_SCOPE)
+                .withWebApkPackageName(WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME)
+                .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT, START_WEBAPK);
+    }
+
+    /**
+     * Test that a link which falls into the scope of an invalid WebAPK (e.g. it was incorrectly
+     * signed) does not get any special WebAPK handling. The first time that the user taps on the
+     * link, the intent picker should be shown.
+     */
+    @SmallTest
+    public void testLaunchWebApk_ShowIntentPickerInvalidWebApk() {
+        CommandLine.getInstance().appendSwitch(ChromeSwitches.ENABLE_WEBAPK);
+        checkUrl(COUNTERFEIT_WEBAPK_SCOPE)
+                .expecting(OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT,
+                        START_OTHER_ACTIVITY);
+    }
+
+    /**
+     * Test that tapping a link which falls into the scope of the current WebAPK stays within the
+     * WebAPK.
+     */
+    @SmallTest
+    public void testLaunchWebApk_StayInSameWebApk() {
+        CommandLine.getInstance().appendSwitch(ChromeSwitches.ENABLE_WEBAPK);
+        checkUrl(WEBAPK_SCOPE + "/new.html")
+                .withReferrer(WEBAPK_SCOPE)
+                .withWebApkPackageName(WEBAPK_PACKAGE_NAME)
+                .expecting(OverrideUrlLoadingResult.NO_OVERRIDE, IGNORE);
+    }
+
     private static ResolveInfo newResolveInfo(String packageName, String name) {
         ActivityInfo ai = new ActivityInfo();
         ai.packageName = packageName;
@@ -788,22 +877,31 @@
                     return list;
                 }
             }
-            if (intent.getDataString().startsWith("http://")
+            String dataString = intent.getDataString();
+            if (dataString.startsWith("http://")
                     || intent.getDataString().startsWith("https://")) {
                 list.add(newResolveInfo("chrome", "chrome"));
             }
-            if (intent.getDataString().startsWith("http://m.youtube.com")
+            if (dataString.startsWith("http://m.youtube.com")
                     || intent.getDataString().startsWith("http://youtube.com")) {
                 list.add(newResolveInfo("youtube", "youtube"));
-            } else if (intent.getDataString().startsWith(PLUS_STREAM_URL)) {
+            } else if (dataString.startsWith(PLUS_STREAM_URL)) {
                 list.add(newResolveInfo("plus", "plus"));
             } else if (intent.getDataString().startsWith(CALENDAR_URL)) {
                 list.add(newResolveInfo("calendar", "calendar"));
-            } else if (intent.getDataString().startsWith("sms")) {
+            } else if (dataString.startsWith("sms")) {
                 list.add(newResolveInfo(
                         TEXT_APP_1_PACKAGE_NAME, TEXT_APP_1_PACKAGE_NAME + ".cls"));
                 list.add(newResolveInfo(
                         TEXT_APP_2_PACKAGE_NAME, TEXT_APP_2_PACKAGE_NAME + ".cls"));
+            } else if (dataString.startsWith(WEBAPK_SCOPE)) {
+                list.add(newResolveInfo(WEBAPK_PACKAGE_NAME, WEBAPK_PACKAGE_NAME));
+            } else if (dataString.startsWith(WEBAPK_WITH_NATIVE_APP_SCOPE)) {
+                list.add(newResolveInfo(WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME,
+                        WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME));
+                list.add(newResolveInfo(NATIVE_APP_PACKAGE_NAME, NATIVE_APP_PACKAGE_NAME));
+            } else if (dataString.startsWith(COUNTERFEIT_WEBAPK_SCOPE)) {
+                list.add(newResolveInfo(COUNTERFEIT_WEBAPK_PACKAGE_NAME, COUNTERFEIT_WEBAPK_SCOPE));
             } else {
                 list.add(newResolveInfo("foo", "foo"));
             }
@@ -817,13 +915,41 @@
 
         @Override
         public boolean isSpecializedHandlerAvailable(List<ResolveInfo> resolveInfos) {
-            for (ResolveInfo resolveInfo : resolveInfos) {
-                String packageName = resolveInfo.activityInfo.packageName;
-                if (packageName.equals("youtube") || packageName.equals("calendar")) {
-                    return true;
+            return countSpecializedHandlers(resolveInfos) > 0;
+        }
+
+        @Override
+        public int countSpecializedHandlers(List<ResolveInfo> infos) {
+            if (infos == null) {
+                return 0;
+            }
+            int count = 0;
+            for (ResolveInfo info : infos) {
+                String packageName = info.activityInfo.packageName;
+                if (packageName.equals("youtube") || packageName.equals("calendar")
+                        || packageName.equals(COUNTERFEIT_WEBAPK_PACKAGE_NAME)
+                        || packageName.equals(NATIVE_APP_PACKAGE_NAME)
+                        || packageName.equals(WEBAPK_PACKAGE_NAME)
+                        || packageName.equals(WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME)) {
+                    ++count;
                 }
             }
-            return false;
+            return count;
+        }
+
+        @Override
+        public String findValidWebApkPackageName(List<ResolveInfo> infos) {
+            if (infos == null) {
+                return null;
+            }
+            for (ResolveInfo info : infos) {
+                String packageName = info.activityInfo.packageName;
+                if (packageName.equals(WEBAPK_PACKAGE_NAME)
+                        || packageName.equals(WEBAPK_WITH_NATIVE_APP_PACKAGE_NAME)) {
+                    return packageName;
+                }
+            }
+            return null;
         }
 
         @Override
@@ -950,7 +1076,7 @@
         private boolean mIsRedirect;
         private boolean mChromeAppInForegroundRequired = true;
         private boolean mIsBackgroundTabNavigation;
-        private boolean mIsWebApk;
+        private String mWebApkPackageName;
         private boolean mHasUserGesture;
         private TabRedirectHandler mRedirectHandler;
 
@@ -958,8 +1084,8 @@
             mUrl = url;
         }
 
-        public ExternalNavigationTestParams withIsWebApk(boolean isWebApk) {
-            mIsWebApk = isWebApk;
+        public ExternalNavigationTestParams withWebApkPackageName(String webApkPackageName) {
+            mWebApkPackageName = webApkPackageName;
             return this;
         }
 
@@ -1009,8 +1135,9 @@
                 int otherExpectation) {
             boolean expectStartIncognito = (otherExpectation & START_INCOGNITO) != 0;
             boolean expectStartActivity =
-                    (otherExpectation & (START_CHROME | START_OTHER_ACTIVITY)) != 0;
+                    (otherExpectation & (START_CHROME | START_WEBAPK | START_OTHER_ACTIVITY)) != 0;
             boolean expectStartChrome = (otherExpectation & START_CHROME) != 0;
+            boolean expectStartWebApk = (otherExpectation & START_WEBAPK) != 0;
             boolean expectStartOtherActivity = (otherExpectation & START_OTHER_ACTIVITY) != 0;
             boolean expectStartFile = (otherExpectation & START_FILE) != 0;
             boolean expectSaneIntent = expectStartOtherActivity
@@ -1025,23 +1152,28 @@
                     .setRedirectHandler(mRedirectHandler)
                     .setIsBackgroundTabNavigation(mIsBackgroundTabNavigation)
                     .setIsMainFrame(true)
-                    .setIsWebApk(mIsWebApk)
+                    .setWebApkPackageName(mWebApkPackageName)
                     .setHasUserGesture(mHasUserGesture)
                     .build();
             OverrideUrlLoadingResult result = mUrlHandler.shouldOverrideUrlLoading(params);
             boolean startActivityCalled = false;
             boolean startChromeCalled = false;
+            boolean startWebApkCalled = false;
             if (mDelegate.startActivityIntent != null) {
                 startActivityCalled = true;
                 String packageName = mDelegate.startActivityIntent.getPackage();
-                startChromeCalled =
-                        packageName != null && packageName.equals(mDelegate.getPackageName());
+                if (packageName != null) {
+                    startChromeCalled = packageName.equals(mDelegate.getPackageName());
+                    startWebApkCalled =
+                            packageName.startsWith(WebApkConstants.WEBAPK_PACKAGE_PREFIX);
+                }
             }
 
             assertEquals(expectedOverrideResult, result);
             assertEquals(expectStartIncognito, mDelegate.startIncognitoIntentCalled);
             assertEquals(expectStartActivity, startActivityCalled);
             assertEquals(expectStartChrome, startChromeCalled);
+            assertEquals(expectStartWebApk, startWebApkCalled);
             assertEquals(expectStartFile, mDelegate.startFileIntentCalled);
 
             if (startActivityCalled && expectSaneIntent) {