Use redirect URL to start webview

NetworkMonitor will detect captive portal and may get a redirect
URL from WiFi AP. Redirect URL should able to send to captive
portal app to open the webview instead of detecting again by
captive portal app.

Bug: 134892996
Test: Manually test with captive portal AP
Test: atest NetworkStackTests NetworkStackNextTests
Change-Id: Idf363c79b7243a899121be8a68b32d0541dff14f
Merged-In: Idf363c79b7243a899121be8a68b32d0541dff14f
diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index d147b45..9d913fc 100644
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -141,6 +141,14 @@
      */
     public static final String DHCP_IP_CONFLICT_DETECT_VERSION = "dhcp_ip_conflict_detect_version";
 
+    /**
+     * Minimum module version at which to enable dismissal CaptivePortalLogin app in validated
+     * network feature. CaptivePortalLogin app will also use validation facilities in
+     * {@link NetworkMonitor} to perform portal validation if feature is enabled.
+     */
+    public static final String DISMISS_PORTAL_IN_VALIDATED_NETWORK =
+            "dismiss_portal_in_validated_network";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
@@ -270,12 +278,32 @@
      */
     public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
             @NonNull String name) {
+        final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
+                0 /* default value */);
+        return isFeatureEnabled(context, namespace, name, false);
+    }
+
+    /**
+     * Check whether or not one specific experimental feature for a particular namespace from
+     * {@link DeviceConfig} is enabled by comparing NetworkStack module version {@link NetworkStack}
+     * with current version of property. If this property version is valid, the corresponding
+     * experimental feature would be enabled, otherwise disabled.
+     * @param context The global context information about an app environment.
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @param defaultEnabled The value to return if the property does not exist or its value is
+     *                       null.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+            @NonNull String name, boolean defaultEnabled) {
         try {
             final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
                     0 /* default value */);
             final long packageVersion = context.getPackageManager().getPackageInfo(
                     context.getPackageName(), 0).getLongVersionCode();
-            return (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
+            return (propertyVersion == 0 && defaultEnabled)
+                    || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
         } catch (NameNotFoundException e) {
             Log.e(TAG, "Could not find the package name", e);
             return false;
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index e914a55..438080c 100644
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -65,6 +65,7 @@
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USER_AGENT;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
+import static android.net.util.NetworkStackUtils.DISMISS_PORTAL_IN_VALIDATED_NETWORK;
 import static android.net.util.NetworkStackUtils.isEmpty;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
@@ -960,7 +961,11 @@
                     final Network network = new Network(mCleartextDnsNetwork);
                     appExtras.putParcelable(ConnectivityManager.EXTRA_NETWORK, network);
                     final CaptivePortalProbeResult probeRes = mLastPortalProbeResult;
-                    appExtras.putString(EXTRA_CAPTIVE_PORTAL_URL, probeRes.detectUrl);
+                    // Use redirect URL from AP if exists.
+                    final String portalUrl =
+                            (useRedirectUrlForPortal() && probeRes.redirectUrl != null)
+                            ? probeRes.redirectUrl : probeRes.detectUrl;
+                    appExtras.putString(EXTRA_CAPTIVE_PORTAL_URL, portalUrl);
                     if (probeRes.probeSpec != null) {
                         final String encodedSpec = probeRes.probeSpec.getEncodedSpec();
                         appExtras.putString(EXTRA_CAPTIVE_PORTAL_PROBE_SPEC, encodedSpec);
@@ -977,6 +982,15 @@
             }
         }
 
+        private boolean useRedirectUrlForPortal() {
+            // It must match the conditions in CaptivePortalLogin in which the redirect URL is not
+            // used to validate that the portal is gone.
+            final boolean aboveQ =
+                    ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
+            return aboveQ && mDependencies.isFeatureEnabled(mContext, NAMESPACE_CONNECTIVITY,
+                    DISMISS_PORTAL_IN_VALIDATED_NETWORK, aboveQ /* defaultEnabled */);
+        }
+
         @Override
         public void exit() {
             if (mLaunchCaptivePortalAppBroadcastReceiver != null) {
@@ -2385,6 +2399,23 @@
                     NetworkMonitorUtils.PERMISSION_ACCESS_NETWORK_CONDITIONS);
         }
 
+        /**
+         * Check whether or not one specific experimental feature for a particular namespace from
+         * {@link DeviceConfig} is enabled by comparing NetworkStack module version
+         * {@link NetworkStack} with current version of property. If this property version is valid,
+         * the corresponding experimental feature would be enabled, otherwise disabled.
+         * @param context The global context information about an app environment.
+         * @param namespace The namespace containing the property to look up.
+         * @param name The name of the property to look up.
+         * @param defaultEnabled The value to return if the property does not exist or its value is
+         *                       null.
+         * @return true if this feature is enabled, or false if disabled.
+         */
+        public boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+                @NonNull String name, boolean defaultEnabled) {
+            return NetworkStackUtils.isFeatureEnabled(context, namespace, name, defaultEnabled);
+        }
+
         public static final Dependencies DEFAULT = new Dependencies();
     }
 
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index b2492aa..72d50b6 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -37,6 +37,7 @@
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS;
 import static android.net.util.NetworkStackUtils.CAPTIVE_PORTAL_USE_HTTPS;
+import static android.net.util.NetworkStackUtils.DISMISS_PORTAL_IN_VALIDATED_NETWORK;
 
 import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_DNS_EVENTS;
 import static com.android.networkstack.apishim.ConstantsShim.DETECTION_METHOD_TCP_METRICS;
@@ -59,6 +60,7 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -478,6 +480,7 @@
 
         mCreatedNetworkMonitors = new HashSet<>();
         mRegisteredReceivers = new HashSet<>();
+        setDismissPortalInValidatedNetwork(false);
     }
 
     @After
@@ -1087,7 +1090,7 @@
     public void testLaunchCaptivePortalApp() throws Exception {
         setSslException(mHttpsConnection);
         setPortal302(mHttpConnection);
-
+        when(mHttpConnection.getHeaderField(eq("location"))).thenReturn(TEST_LOGIN_URL);
         final NetworkMonitor nm = makeMonitor(METERED_CAPABILITIES);
         notifyNetworkConnected(nm, METERED_CAPABILITIES);
 
@@ -1111,6 +1114,9 @@
         // framework and only intended for the captive portal app, but the framework needs
         // the network to identify the right NetworkMonitor.
         assertEquals(TEST_NETID, networkCaptor.getValue().netId);
+        // Portal URL should be detection URL.
+        final String redirectUrl = bundle.getString(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
+        assertEquals(TEST_HTTP_URL, redirectUrl);
 
         // Have the app report that the captive portal is dismissed, and check that we revalidate.
         setStatus(mHttpsConnection, 204);
@@ -1443,6 +1449,50 @@
     }
 
     @Test
+    public void testDismissPortalInValidatedNetworkEnabledOsSupported() throws Exception {
+        assumeTrue(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDismissPortalInValidatedNetworkEnabled(TEST_LOGIN_URL);
+    }
+
+    @Test
+    public void testDismissPortalInValidatedNetworkEnabledOsNotSupported() throws Exception {
+        assumeFalse(ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q));
+        testDismissPortalInValidatedNetworkEnabled(TEST_HTTP_URL);
+    }
+
+    private void testDismissPortalInValidatedNetworkEnabled(String portalUrl) throws Exception {
+        setDismissPortalInValidatedNetwork(true);
+        setSslException(mHttpsConnection);
+        setPortal302(mHttpConnection);
+        when(mHttpConnection.getHeaderField(eq("location"))).thenReturn(TEST_LOGIN_URL);
+        final NetworkMonitor nm = makeMonitor(METERED_CAPABILITIES);
+        notifyNetworkConnected(nm, METERED_CAPABILITIES);
+
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+            .showProvisioningNotification(any(), any());
+
+        assertEquals(1, mRegisteredReceivers.size());
+        // Check that startCaptivePortalApp sends the expected intent.
+        nm.launchCaptivePortalApp();
+
+        final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
+        verify(mCm, timeout(HANDLER_TIMEOUT_MS).times(1))
+            .startCaptivePortalApp(networkCaptor.capture(), bundleCaptor.capture());
+        verify(mNotifier).notifyCaptivePortalValidationPending(networkCaptor.getValue());
+        final Bundle bundle = bundleCaptor.getValue();
+        final Network bundleNetwork = bundle.getParcelable(ConnectivityManager.EXTRA_NETWORK);
+        assertEquals(TEST_NETID, bundleNetwork.netId);
+        // Network is passed both in bundle and as parameter, as the bundle is opaque to the
+        // framework and only intended for the captive portal app, but the framework needs
+        // the network to identify the right NetworkMonitor.
+        assertEquals(TEST_NETID, networkCaptor.getValue().netId);
+        // Portal URL should be redirect URL.
+        final String redirectUrl = bundle.getString(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL);
+        assertEquals(portalUrl, redirectUrl);
+    }
+
+    @Test
     public void testEvaluationState_clearProbeResults() throws Exception {
         final NetworkMonitor nm = runValidatedNetworkTest();
         nm.getEvaluationState().clearProbeResults();
@@ -1602,6 +1652,11 @@
                 eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())).thenReturn(mode);
     }
 
+    private void setDismissPortalInValidatedNetwork(boolean enabled) {
+        when(mDependencies.isFeatureEnabled(any(), any(),
+                eq(DISMISS_PORTAL_IN_VALIDATED_NETWORK), anyBoolean())).thenReturn(enabled);
+    }
+
     private void runPortalNetworkTest(int result) {
         runNetworkTest(result);
         assertEquals(1, mRegisteredReceivers.size());