/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.captiveportallogin;

import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.ACTION_CREATE_DOCUMENT;
import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN;
import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL;
import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL;
import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT;
import static android.net.ConnectivityManager.EXTRA_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.view.accessibility.AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;

import static androidx.browser.customtabs.CustomTabsCallback.NAVIGATION_STARTED;
import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION;
import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static androidx.test.espresso.Espresso.pressBack;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasPackage;
import static androidx.test.espresso.intent.matcher.IntentMatchers.isInternal;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.captiveportallogin.CaptivePortalLoginActivity.EXTRA_USE_CLASSIC_VIEW;
import static com.android.captiveportallogin.CaptivePortalLoginFlags.CAPTIVE_PORTAL_CUSTOM_TABS;
import static com.android.captiveportallogin.DownloadService.DOWNLOAD_ABORTED_REASON_FILE_TOO_LARGE;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_FEATURE_NOT_ENABLED;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_PRIVATE_DNS_ENABLED_V_AND_BELOW;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_NOT_SUPPORT_MULTI_NETWORK;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_RUNNING_ANDROID_R;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_UNKNOWN;
import static com.android.os.corenetworking.captiveportallogin.CaptivePortalLoginStatsLog.CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_USE_CLASSIC_VIEW;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.core.AllOf.allOf;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Instrumentation.ActivityResult;
import android.app.KeyguardManager;
import android.app.UiAutomation;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.CaptivePortal;
import android.net.CaptivePortalData;
import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.ServiceSpecificException;
import android.system.OsConstants;
import android.util.ArrayMap;
import android.view.accessibility.AccessibilityEvent;
import android.webkit.WebView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.EngagementSignalsCallback;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;

import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestNetworkTracker;
import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;

import fi.iki.elonen.NanoHTTPD;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class CaptivePortalLoginActivityTest {
    private static final String TEST_URL = "http://android.test.com";
    private static final int TEST_NETID = 1234;
    private static final int TEST_CUSTOM_TABS_PROVIDER_UID = 12345;
    private static final String TEST_NC_SSID = "Test NetworkCapabilities SSID";
    private static final String TEST_WIFIINFO_SSID = "Test Other SSID";
    private static final String TEST_URL_QUERY = "testquery";
    private static final long TEST_TIMEOUT_MS = 30_000L;
    private static final LinkAddress TEST_LINKADDR = new LinkAddress(
            InetAddresses.parseNumericAddress("2001:db8::8"), 64);
    private static final String TEST_USERAGENT = "Test/42.0 Unit-test";
    private static final String TEST_FRIENDLY_NAME = "Network friendly name";
    private static final String TEST_PORTAL_HOSTNAME = "localhost";
    private static final String TEST_CUSTOM_TABS_PACKAGE_NAME = "com.android.customtabs";
    private static final String TEST_CUSTOM_TABS_CLASS = "com.android.customtabs.MockCustomTabs";
    private static final String TEST_WIFI_CONFIG_TYPE = "application/x-wifi-config";
    private static final String TEST_PRIVATE_DNS_SERVER = "dns.server";
    private static final String TEST_DOWNLOAD_SERVICE_COMPONENT_CLASS_NAME =
            DownloadService.class.getName();
    private static final int CUSTOM_TAB_MENU_ITEM_DO_NOT_USE_THIS_NETWORK = 0;
    private static final int CUSTOM_TAB_MENU_ITEM_USE_THIS_NETWORK = 1;
    private static final int CUSTOM_TAB_MENU_ITEM_USE_CLASSIC_VIEW = 2;

    private ActivityScenario<InstrumentedCaptivePortalLoginActivity> mActivityScenario;
    private Network mNetwork = new Network(TEST_NETID);
    private TestNetworkTracker mTestNetworkTracker;

    private @Spy DownloadService mDownloadService = new DownloadService();

    private static ConnectivityManager sConnectivityManager;
    private static DevicePolicyManager sMockDevicePolicyManager;
    private static DownloadService.DownloadServiceBinder sDownloadServiceBinder;
    private static CustomTabsClient sMockCustomTabsClient;
    private static CaptivePortalLoginMetrics sMockCaptivePortalLoginMetrics;
    private static final ArrayMap<String, Boolean> sFeatureFlags = new ArrayMap<>();
    private static boolean sIsMultiNetworkingSupportedByProvider;
    private static Bundle sIntentExtrasForceWebview;
    private static Network sNetworkForceWebview;
    @Rule
    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
            new SetFeatureFlagsRule((name, enabled) -> {
                sFeatureFlags.put(name, enabled);
                return null;
            }, (name) -> sFeatureFlags.getOrDefault(name, false));
    @Rule
    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();

    public static class InstrumentedCaptivePortalLoginActivity extends CaptivePortalLoginActivity {
        private final ConditionVariable mDestroyedCv = new ConditionVariable(false);
        private final CompletableFuture<Intent> mForegroundServiceStart = new CompletableFuture<>();
        // Workaround for https://github.com/android/android-test/issues/1119
        private final CompletableFuture<Intent> mOpenInBrowserIntent =
                new CompletableFuture<>();
        private Intent mServiceIntent = new Intent();
        private final CompletableFuture<ServiceConnection> mDownloadServiceBound =
                new CompletableFuture<>();
        private final CompletableFuture<CustomTabsServiceConnection> mCustomTabsServiceBound =
                new CompletableFuture<>();
        private final ConditionVariable mDlServiceunbindCv = new ConditionVariable(false);

        @Override
        public Object getSystemService(String name) {
            switch (name) {
                case Context.CONNECTIVITY_SERVICE:
                    return sConnectivityManager;
                case Context.DEVICE_POLICY_SERVICE:
                    return sMockDevicePolicyManager;
                default:
                    return super.getSystemService(name);
            }
        }

        @Override
        WifiInfo getWifiConnectionInfo() {
            return makeWifiInfo();
        }

        @Override
        public ComponentName startForegroundService(Intent service) {
            assertTrue("Multiple foreground services were started during the test",
                    mForegroundServiceStart.complete(service));
            // Do not actually start the service
            return service.getComponent();
        }

        @Override
        public ComponentName startService(Intent service) {
            mServiceIntent = service;
            // Do not actually start the service
            return service.getComponent();
        }

        @Override
        public void onDestroy() {
            super.onDestroy();
            mDestroyedCv.open();
        }

        @Override
        public boolean bindService(Intent service, ServiceConnection conn, int flags) {
            if (service.getAction() == null
                    && service.getComponent().getClassName().equals(
                            TEST_DOWNLOAD_SERVICE_COMPONENT_CLASS_NAME)) {
                assertTrue("Download foreground service was bound multiple times during the test",
                        mDownloadServiceBound.complete(conn));
                getMainThreadHandler().post(() -> conn.onServiceConnected(
                        getInstrumentation().getComponentName(), sDownloadServiceBinder));
            } else if (service.getAction().equals(ACTION_CUSTOM_TABS_CONNECTION)) {
                assertTrue("CustomTabs foreground service was bound multiple times during the test",
                        mCustomTabsServiceBound.complete((CustomTabsServiceConnection) conn));
                getMainThreadHandler().post(() -> {
                    ((CustomTabsServiceConnection) conn).onCustomTabsServiceConnected(
                            getInstrumentation().getComponentName(), sMockCustomTabsClient);
                });
            }
            return true;
        }

        @Override
        public void unbindService(ServiceConnection conn) {
            mDlServiceunbindCv.open();
        }

        @Override
        Context getContextForCustomTabsBinding() {
            return this;
        }

        @Override
        public void startActivity(Intent intent) {
            if (Intent.ACTION_VIEW.equals(intent.getAction())
                    && intent.getData() != null
                    && intent.getData().getAuthority().startsWith(TEST_PORTAL_HOSTNAME)) {
                mOpenInBrowserIntent.complete(intent);
                return;
            }
            super.startActivity(intent);
        }

        @Override
        String getFileProviderAuthority() {
            // Matches the test provider in the test app manifest
            return "com.android.captiveportallogin.tests.fileprovider";
        }

        @Override
        CaptivePortalLoginMetrics getCaptivePortalLoginMetrics() {
            return sMockCaptivePortalLoginMetrics;
        }

        @Override
        String getDefaultCustomTabsProviderPackage() {
            return TEST_CUSTOM_TABS_PACKAGE_NAME;
        }

        @Override
        int getPackageUid(@NonNull final String customTabsProviderPackageName)
                throws NameNotFoundException {
            return TEST_CUSTOM_TABS_PROVIDER_UID;
        }

        @Override
        boolean isMultiNetworkingSupportedByProvider(final String defaultPackageName) {
            return sIsMultiNetworkingSupportedByProvider;
        }

        @Override
        boolean isFeatureEnabled(final String name) {
            if (sFeatureFlags.get(name) == null) return false;
            return sFeatureFlags.get(name);
        }

        @Override
        boolean isFeatureNotChickenedOut(String name) {
            if (sFeatureFlags.get(name) == null) return true;
            return sFeatureFlags.get(name);
        }

        @Override
        boolean getDeviceConfigPropertyBoolean(final String name, boolean defaultValue) {
            return defaultValue;
        }

        @Override
        void startActivityFromCustomTabsMenuItem(@NonNull final Network network,
                @NonNull final Bundle intentExtras) {
            // Do not start a new captive portal activity.
            sNetworkForceWebview = network;
            sIntentExtrasForceWebview = intentExtras;
        }
    }

    /** Class to replace CaptivePortal to prevent mock object is updated and replaced by parcel. */
    public static class MockCaptivePortal extends CaptivePortal {
        public OutcomeReceiver<Void, ServiceSpecificException> mDelegateUidReceiver;

        int mDismissTimes;
        int mIgnoreTimes;
        int mUseTimes;
        int mReevaluateTimes;
        int mSetDelegateUidTimes;

        private MockCaptivePortal() {
            this(0, 0, 0, 0, 0);
        }
        private MockCaptivePortal(int dismissTimes, int ignoreTimes, int useTimes,
                int reevaluateTimes, int setDelegateUidTimes) {
            super(null);
            mDismissTimes = dismissTimes;
            mIgnoreTimes = ignoreTimes;
            mUseTimes = useTimes;
            mReevaluateTimes = reevaluateTimes;
            mSetDelegateUidTimes = setDelegateUidTimes;
        }

        @Override
        public void reportCaptivePortalDismissed() {
            mDismissTimes++;
        }

        @Override
        public void ignoreNetwork() {
            mIgnoreTimes++;
        }

        @Override
        public void useNetwork() {
            mUseTimes++;
        }

        @Override
        public void reevaluateNetwork() {
            mReevaluateTimes++;
        }

        @Override
        public void setDelegateUid(int uid, Executor executor, OutcomeReceiver receiver) {
            mDelegateUidReceiver = receiver;
            mSetDelegateUidTimes++;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(mDismissTimes);
            out.writeInt(mIgnoreTimes);
            out.writeInt(mUseTimes);
            out.writeInt(mReevaluateTimes);
        }

        public static final Parcelable.Creator<MockCaptivePortal> CREATOR =
                new Parcelable.Creator<MockCaptivePortal>() {
                @Override
                public MockCaptivePortal createFromParcel(Parcel in) {
                    return new MockCaptivePortal(in.readInt(), in.readInt(), in.readInt(),
                            in.readInt(), in.readInt());
                }

                @Override
                public MockCaptivePortal[] newArray(int size) {
                    return new MockCaptivePortal[size];
                }
        };
    }

    @Before
    public void setUp() throws Exception {
        final Context context = getInstrumentation().getContext();
        sConnectivityManager = spy(context.getSystemService(ConnectivityManager.class));
        sMockDevicePolicyManager = mock(DevicePolicyManager.class);
        sDownloadServiceBinder = mock(DownloadService.DownloadServiceBinder.class);
        sMockCustomTabsClient = mock(CustomTabsClient.class);
        sMockCaptivePortalLoginMetrics = mock(CaptivePortalLoginMetrics.class);

        MockitoAnnotations.initMocks(this);
        // Use a real (but test) network for the application. The application will pass this
        // network to ConnectivityManager#bindProcessToNetwork, so it needs to be a real, existing
        // network on the device but otherwise has no functional use at all. The http server set up
        // by this test will run on the loopback interface and will not use this test network.
        final UiAutomation automation = getInstrumentation().getUiAutomation();
        automation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS);
        try {
            mTestNetworkTracker = initTestNetwork(
                    getInstrumentation().getContext(), TEST_LINKADDR, TEST_TIMEOUT_MS);
        } finally {
            automation.dropShellPermissionIdentity();
        }
        mNetwork = mTestNetworkTracker.getNetwork();
        doReturn(CustomTabsSession.createMockSessionForTesting(new ComponentName(
                TEST_CUSTOM_TABS_PACKAGE_NAME, TEST_CUSTOM_TABS_CLASS))
        ).when(sMockCustomTabsClient).newSession(any());
    }

    private static WifiInfo makeWifiInfo() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            fail("Only Q should be using WifiInfo; R+ gets the wifi SSID via NetworkCapabilities");
        }

        // WifiInfo did not have a builder before R. Use non-public APIs on Q to set SSID.
        try {
            final WifiInfo info = WifiInfo.class.getConstructor().newInstance();
            final Class<?> wifiSsidClass = Class.forName("android.net.wifi.WifiSsid");
            final Object wifiSsid = wifiSsidClass.getMethod("createFromAsciiEncoded",
                    String.class).invoke(null, TEST_WIFIINFO_SSID);
            WifiInfo.class.getMethod("setSSID", wifiSsidClass).invoke(info, wifiSsid);
            return info;
        } catch (ReflectiveOperationException e) {
            throw new AssertionError("Failed to create WifiInfo on Q", e);
        }
    }

    @After
    public void tearDown() throws Exception {
        if (mActivityScenario != null) {
            // Note this may sometimes block for 45 seconds until
            // https://github.com/android/android-test/issues/676 is fixed
            mActivityScenario.close();
            Intents.release();
            getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
        }
        getInstrumentation().getUiAutomation().setOnAccessibilityEventListener(null);
        getInstrumentation().getContext().getSystemService(ConnectivityManager.class)
                .bindProcessToNetwork(null);
        if (mTestNetworkTracker != null) {
            runAsShell(MANAGE_TEST_NETWORKS, mTestNetworkTracker::teardown);
        }
    }

    private Intent makeIntent(Context context, String url, boolean useClassicView,
            Parcelable captivePortal) {
        return new Intent(context, InstrumentedCaptivePortalLoginActivity.class)
                .setAction(ACTION_CAPTIVE_PORTAL_SIGN_IN)
                .putExtra(EXTRA_CAPTIVE_PORTAL_URL, url)
                .putExtra(EXTRA_NETWORK, mNetwork)
                .putExtra(EXTRA_CAPTIVE_PORTAL_USER_AGENT, TEST_USERAGENT)
                .putExtra(EXTRA_CAPTIVE_PORTAL, captivePortal)
                .putExtra(EXTRA_USE_CLASSIC_VIEW, useClassicView);
    }

    private void initActivity(String url) {
        initActivity(url, false /* useClassicView */);
    }

    private void initActivity(String url, boolean useClassicView) {
        final Context ctx = getInstrumentation().getContext();
        final Intent intent = makeIntent(ctx, url, useClassicView, new MockCaptivePortal());
        mActivityScenario = ActivityScenario.launch(intent);
        mActivityScenario.onActivity(activity -> {
            getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                    android.Manifest.permission.POST_NOTIFICATIONS);
            ctx.getSystemService(KeyguardManager.class).requestDismissKeyguard(activity, null);
            // Dismiss dialogs or notification shade, so the test can interact with the activity.
            activity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
        });
        getInstrumentation().waitForIdleSync();
    }

    @Test
    public void testonCreateWithNullCaptivePortal() throws Exception {
        final Context ctx = getInstrumentation().getContext();
        final Intent intent = makeIntent(ctx, TEST_URL, false /* useClassicView */,
                (Bundle) null /* CaptivePortal */);
        try (ActivityScenario<InstrumentedCaptivePortalLoginActivity> scenario =
                     ActivityScenario.launch(intent)) {
            getInstrumentation().waitForIdleSync();
            // Verify that activity calls finish() immediately in its onCreate
            assertEquals(DESTROYED, scenario.getState());
        }
    }

    /**
     * Get the activity MockCaptivePortal.
     *
     * The activity may use a different MockCaptivePortal instance after being recreated, so the
     * MockCaptivePortal should not be kept across possible activity recreation.
     */
    private MockCaptivePortal getCaptivePortal() {
        final AtomicReference<MockCaptivePortal> portalRef = new AtomicReference<>();
        mActivityScenario.onActivity(a -> portalRef.set((MockCaptivePortal) a.mCaptivePortal));
        return portalRef.get();
    }

    private void configNonVpnNetwork() {
        final Network[] networks = new Network[] {new Network(mNetwork)};
        doReturn(networks).when(sConnectivityManager).getAllNetworks();
        final NetworkCapabilities nonVpnCapabilities;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            // SSID and NetworkCapabilities builder was added in R
            nonVpnCapabilities = new NetworkCapabilities.Builder()
                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                    .setSsid(TEST_NC_SSID)
                    .build();
        } else {
            nonVpnCapabilities = new NetworkCapabilities()
                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
        }
        doReturn(nonVpnCapabilities).when(sConnectivityManager).getNetworkCapabilities(
                mNetwork);
    }

    private void configVpnNetwork() {
        final Network network1 = new Network(TEST_NETID + 1);
        final Network network2 = new Network(TEST_NETID + 2);
        final Network[] networks = new Network[] {network1, network2};
        doReturn(networks).when(sConnectivityManager).getAllNetworks();
        final NetworkCapabilities underlyingCapabilities = new NetworkCapabilities()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
        final NetworkCapabilities vpnCapabilities = new NetworkCapabilities(underlyingCapabilities)
                .addTransportType(NetworkCapabilities.TRANSPORT_VPN);
        doReturn(underlyingCapabilities).when(sConnectivityManager).getNetworkCapabilities(
                network1);
        doReturn(vpnCapabilities).when(sConnectivityManager).getNetworkCapabilities(network2);
    }

    @Test
    public void testHasVpnNetwork() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // Test non-vpn case.
        configNonVpnNetwork();
        mActivityScenario.onActivity(activity -> assertFalse(activity.hasVpnNetwork()));

        // Test vpn case.
        configVpnNetwork();
        mActivityScenario.onActivity(activity -> assertTrue(activity.hasVpnNetwork()));
    }

    @Test
    public void testIsAlwaysOnVpnEnabled() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        doReturn(false).when(sMockDevicePolicyManager).isAlwaysOnVpnLockdownEnabled(any());
        mActivityScenario.onActivity(activity -> assertFalse(activity.isAlwaysOnVpnEnabled()));

        doReturn(true).when(sMockDevicePolicyManager).isAlwaysOnVpnLockdownEnabled(any());
        mActivityScenario.onActivity(activity -> assertTrue(activity.isAlwaysOnVpnEnabled()));
    }

    private void runVpnMsgOrLinkToBrowser(boolean useVpnMatcher) {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // Test non-vpn case.
        configNonVpnNetwork();
        doReturn(false).when(sMockDevicePolicyManager).isAlwaysOnVpnLockdownEnabled(any());
        final String linkMatcher = ".*<a[^>]+href.*";
        mActivityScenario.onActivity(act ->
                assertTrue(act.getWebViewClient().getVpnMsgOrLinkToBrowser().matches(linkMatcher)));

        // Test has vpn case.
        configVpnNetwork();
        final String vpnMatcher = ".*<div.*vpnwarning.*";
        mActivityScenario.onActivity(act ->
                assertTrue(act.getWebViewClient().getVpnMsgOrLinkToBrowser().matches(vpnMatcher)));

        // Test always-on vpn case.
        configNonVpnNetwork();
        doReturn(true).when(sMockDevicePolicyManager).isAlwaysOnVpnLockdownEnabled(any());
        mActivityScenario.onActivity(act ->
                assertTrue(act.getWebViewClient().getVpnMsgOrLinkToBrowser().matches(
                        (useVpnMatcher ? vpnMatcher : linkMatcher))));
    }

    @Test @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
    public void testVpnMsgOrLinkToBrowser_BeforeR() throws Exception {
        // Before Android R, CaptivePortalLogin cannot call isAlwaysOnVpnLockdownEnabled() due to
        // permission denied. So CaptivePortalLogin doesn't know the status of VPN always-on, and it
        // simply provides a link for user to open the browser as usual.
        runVpnMsgOrLinkToBrowser(false /* useVpnMatcher */);
    }

    @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
    public void testVpnMsgOrLinkToBrowser() throws Exception {
        // After Android R(including), DevicePolicyManager allows the caller who has the
        // PERMISSION_MAINLINE_NETWORK_STACK can call the isAlwaysOnVpnLockdownEnabled() to get the
        // status of VPN always-on. So the CaptivePortalLogin could know the status of VPN always-on
        // and show the related warning message to the user.
        runVpnMsgOrLinkToBrowser(true /* useVpnMatcher */);
    }

    private void notifyCapabilitiesChanged(final NetworkCapabilities nc) {
        mActivityScenario.onActivity(a -> a.handleCapabilitiesChanged(mNetwork, nc));
        getInstrumentation().waitForIdleSync();
    }

    private void notifyLinkPropertiesChanged(final LinkProperties lp) {
        mActivityScenario.onActivity(a -> a.handleLinkPropertiesChanged(mNetwork, lp));
        getInstrumentation().waitForIdleSync();
    }

    private void notifyValidatedChangedAndDismissed(final NetworkCapabilities nc) {
        // Get the MockCaptivePortal before the activity destroys itself
        final MockCaptivePortal cp = getCaptivePortal();
        notifyCapabilitiesChanged(nc);

        assertEquals(cp.mDismissTimes, 1);
        assertEquals(cp.mIgnoreTimes, 0);
        assertEquals(cp.mUseTimes, 0);
    }

    private void verifyNotDone() {
        final MockCaptivePortal cp = getCaptivePortal();
        assertEquals(cp.mDismissTimes, 0);
        assertEquals(cp.mIgnoreTimes, 0);
        assertEquals(cp.mUseTimes, 0);
    }

    private void notifyValidatedChangedNotDone(final NetworkCapabilities nc) {
        notifyCapabilitiesChanged(nc);
        verifyNotDone();
    }

    void waitForDestroyedState() throws Exception {
        final long startTimeMs = System.currentTimeMillis();
        long currentTimeMs = startTimeMs;
        while (mActivityScenario.getState() != DESTROYED
                && (currentTimeMs - startTimeMs) < TEST_TIMEOUT_MS) {
            Thread.sleep(50);
            currentTimeMs = System.currentTimeMillis();
        }
    }

    @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
    public void testNetworkCapabilitiesUpdate_RAndLater() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // NetworkCapabilities updates w/o NET_CAPABILITY_VALIDATED.
        final NetworkCapabilities nc = new NetworkCapabilities();
        notifyValidatedChangedNotDone(nc);

        // NetworkCapabilities updates w/ NET_CAPABILITY_VALIDATED.
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);

        // Workaround to deflake the test. The problem may be caused by a race with lock inside
        // InstrumentationActivityInvoker.
        // TODO: Remove it once https://github.com/android/android-test/issues/676 is fixed.
        waitForDestroyedState();
    }

    @Test @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
    public void testNetworkCapabilitiesUpdate_Q() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        // Auto-dismiss should not happen.
        notifyValidatedChangedNotDone(nc);
    }

    private HttpServer runCustomSchemeTest(String linkUri) throws Exception {
        final HttpServer server = new HttpServer();
        server.setResponseBody(TEST_URL_QUERY,
                "<a id='tst_link' href='" + linkUri + "'>Test link</a>");

        server.start();
        ActivityScenario.launch(RequestDismissKeyguardActivity.class);
        initActivity(server.makeUrl(TEST_URL_QUERY));
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();
        // Mock all external intents
        intending(not(isInternal())).respondWith(new ActivityResult(RESULT_OK, null));

        onWebView().withElement(findElement(Locator.ID, "tst_link")).perform(webClick());
        getInstrumentation().waitForIdleSync();
        return server;
    }

    @Test
    public void testTelScheme() throws Exception {
        final String telUri = "tel:0123456789";
        final HttpServer server = runCustomSchemeTest(telUri);

        final Intent sentIntent = Intents.getIntents().get(0);
        assertEquals(Intent.ACTION_DIAL, sentIntent.getAction());
        assertEquals(Uri.parse(telUri), sentIntent.getData());

        server.stop();
    }

    @Test
    public void testSmsScheme() throws Exception {
        final String telUri = "sms:0123456789";
        final HttpServer server = runCustomSchemeTest(telUri);

        final Intent sentIntent = Intents.getIntents().get(0);
        assertEquals(Intent.ACTION_SENDTO, sentIntent.getAction());
        assertEquals(Uri.parse(telUri), sentIntent.getData());

        server.stop();
    }

    @Test
    public void testUnsupportedScheme() throws Exception {
        final HttpServer server = runCustomSchemeTest("mailto:test@example.com");
        assertEquals(0, Intents.getIntents().size());

        // Mockito intents cannot be used for an intent sent in onDestroy, due to
        // https://github.com/android/android-test/issues/1119
        final CompletableFuture<Intent> viewIntent = new CompletableFuture<>();
        mActivityScenario.onActivity(a -> a.mOpenInBrowserIntent.thenAccept(viewIntent::complete));

        final MockCaptivePortal cp = getCaptivePortal();
        onWebView().withElement(findElement(Locator.ID, "continue_link"))
                .perform(webClick());

        try {
            viewIntent.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new AssertionError(
                    "Open in browser intent not received after " + TEST_TIMEOUT_MS + "ms", e);
        }

        getInstrumentation().waitForIdleSync();
        assertEquals(DESTROYED, mActivityScenario.getState());

        assertEquals(cp.mDismissTimes, 0);
        assertEquals(cp.mIgnoreTimes, 0);
        assertEquals(cp.mUseTimes, 1);
        server.stop();
    }

    @Test
    public void testWebViewBackButton() throws Exception {
        final HttpServer server = new HttpServer();
        final String page1Query = "index.html";
        final String page2Query = "some_other.html";
        final String page1LinkId = "page1_link";
        final String page1LinkText = "Go to some other page";
        final String page2ContentId = "page2_content";
        final String page2ContentText = "This is the other page";
        server.start();

        // page1 contains a link that takes the whole page so clicking anywhere in the webview
        // works, this configuration is necessary to make the UiDevice based click work.
        server.setResponseBody(page1Query,
                "<p><a style=\"position: absolute; display: block; top: 0; left: 0; width: 100%; "
                        + "height: 100%;\" href=\""
                        + page2Query + "\" id='" + page1LinkId
                        + "'>" + page1LinkText + "</a></p>");
        server.setResponseBody(page2Query,
                "<p id='" + page2ContentId + "'>" + page2ContentText + "</p>");
        ActivityScenario.launch(RequestDismissKeyguardActivity.class);
        initActivity(server.makeUrl(page1Query));
        Intents.init();
        // Check page 1 is loaded.
        onWebView().withElement(findElement(Locator.ID, page1LinkId))
                .check(webMatches(getText(), containsString(page1LinkText)));

        // Use the actually click event to perform the navigation.
        // This is a hack to work around the issue that webClick() doesn't properly store
        // navigation history in the webview during testing, so we'll simulate an actual user
        // click instead.
        final UiDevice device = UiDevice.getInstance(getInstrumentation());
        device.findObject(By.clazz(WebView.class.getName())).click();

        // Wait for the second page to load by waiting for the webview to be able to go back.
        final AtomicReference<WebView> webViewRef = new AtomicReference<>();
        mActivityScenario.onActivity(activity -> webViewRef.set(
                activity.findViewById(R.id.webview)));
        final AtomicBoolean canGoBack = new AtomicBoolean(false);
        assertTrue("Timeout waiting for second page to load", isEventually(() -> {
            getInstrumentation().runOnMainSync(() -> {
                if (webViewRef.get() != null) {
                    canGoBack.set(webViewRef.get().canGoBack());
                }
            });
            return canGoBack.get();
        }, TEST_TIMEOUT_MS));

        // Check the second page is loaded.
        onWebView().withElement(findElement(Locator.ID, page2ContentId))
                .check(webMatches(getText(), containsString(page2ContentText)));

        // Now that a history entry for page2 exists, pressing back should navigate back.
        pressBack();

        // Check we are back on page 1
        onWebView().withElement(findElement(Locator.ID, page1LinkId))
                .check(webMatches(getText(), containsString(page1LinkText)));
        server.stop();
    }

    @Test
    public void testDownload() throws Exception {
        // Setup the server with a single link on the portal page, leading to a download
        final HttpServer server = new HttpServer();
        final String linkIdDownload = "download";
        final String downloadQuery = "dl";
        final String filename = "testfile.png";
        final String mimetype = "image/png";
        server.setResponseBody(TEST_URL_QUERY,
                "<a id='" + linkIdDownload + "' href='" + downloadQuery + "'>Download</a>");
        server.setResponse(downloadQuery, "This is a test file", mimetype, Collections.singletonMap(
                "Content-Disposition", "attachment; filename=\"" + filename + "\""));
        server.start();

        ActivityScenario.launch(RequestDismissKeyguardActivity.class);
        initActivity(server.makeUrl(TEST_URL_QUERY));
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // Create a mock file to be returned when mocking the file chooser
        final Intent mockFileResponse = new Intent();
        final Uri mockFile = Uri.parse("content://mockdata");
        mockFileResponse.setData(mockFile);

        // Mock file chooser and DownloadService intents
        intending(hasAction(ACTION_CREATE_DOCUMENT)).respondWith(
                new ActivityResult(RESULT_OK, mockFileResponse));
        // No intent fired yet
        assertEquals(0, Intents.getIntents().size());

        onWebView().withElement(findElement(Locator.ID, linkIdDownload))
                .perform(webClick());

        // The create file intent should be fired when the download starts
        assertTrue("Create file intent not received within timeout",
                isEventually(() -> Intents.getIntents().size() == 1, TEST_TIMEOUT_MS));

        final Intent fileIntent = Intents.getIntents().get(0);
        assertEquals(ACTION_CREATE_DOCUMENT, fileIntent.getAction());
        assertEquals(mimetype, fileIntent.getType());
        assertEquals(filename, fileIntent.getStringExtra(Intent.EXTRA_TITLE));

        // The download intent should be fired after the create file result is received
        final CompletableFuture<Intent> dlIntentFuture = new CompletableFuture<>();
        mActivityScenario.onActivity(a ->
                a.mForegroundServiceStart.thenAccept(dlIntentFuture::complete));

        final String expectedUrl = server.makeUrl(downloadQuery);
        verify(sDownloadServiceBinder, times(1)).requestDownload(eq(mNetwork),
                any() /* userAgent */, eq(expectedUrl), eq(filename),
                eq(mockFile), any() /* context */, eq(mimetype));

        server.stop();
    }

    @Test
    public void testVenueFriendlyNameTitle() throws Exception {
        final LinkProperties linkProperties = new LinkProperties();
        CaptivePortalData.Builder captivePortalDataBuilder = new CaptivePortalData.Builder();
        // TODO: Use reflection for setVenueFriendlyName until shims are available
        final Class captivePortalDataBuilderClass = captivePortalDataBuilder.getClass();
        final Method setVenueFriendlyNameMethod;

        setVenueFriendlyNameMethod = captivePortalDataBuilderClass.getDeclaredMethod(
                "setVenueFriendlyName", CharSequence.class);

        captivePortalDataBuilder = (CaptivePortalData.Builder)
                setVenueFriendlyNameMethod.invoke(captivePortalDataBuilder, TEST_FRIENDLY_NAME);

        final CaptivePortalData captivePortalData = captivePortalDataBuilder.build();
        linkProperties.setCaptivePortalData(captivePortalData);

        when(sConnectivityManager.getLinkProperties(mNetwork)).thenReturn(linkProperties);
        configNonVpnNetwork();
        initActivity("https://tc.example.com/");
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // Verify that the correct venue friendly name is used
        mActivityScenario.onActivity(activity ->
                assertEquals(getInstrumentation().getContext().getString(R.string.action_bar_title,
                        TEST_FRIENDLY_NAME), activity.getActionBar().getTitle()));
    }

    @Test @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
    public void testWifiSsid_Q() throws Exception {
        configNonVpnNetwork();
        initActivity("https://portal.example.com/");
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();
        mActivityScenario.onActivity(activity ->
                assertEquals(activity.getActionBar().getTitle(),
                        getInstrumentation().getContext().getString(R.string.action_bar_title,
                                TEST_WIFIINFO_SSID)));

    }

    @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
    public void testWifiSsid() throws Exception {
        configNonVpnNetwork();
        initActivity("https://portal.example.com/");
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();
        mActivityScenario.onActivity(activity ->
                assertEquals(activity.getActionBar().getTitle(),
                        getInstrumentation().getContext().getString(R.string.action_bar_title,
                                TEST_NC_SSID)));
    }

    /**
     * Check whether the device release or development API level is strictly higher than the passed
     * in level.
     *
     * @return True if the device supports an SDK that has or will have a higher version number,
     *         even if still in development.
     */
    private static boolean isReleaseOrDevelopmentApiAbove(int apiLevel) {
        // In-development API after n may have SDK_INT == n and CODENAME != REL
        // Stable API n has SDK_INT == n and CODENAME == REL.
        final int devApiLevel = Build.VERSION.SDK_INT
                + ("REL".equals(Build.VERSION.CODENAME) ? 0 : 1);
        return devApiLevel > apiLevel;
    }

    private static boolean isEventually(BooleanSupplier condition, long timeout)
            throws InterruptedException {
        final long start = System.currentTimeMillis();
        do {
            if (condition.getAsBoolean()) return true;
            Thread.sleep(10);
        } while ((System.currentTimeMillis() - start) < timeout);

        return false;
    }

    private static class HttpServer extends NanoHTTPD {
        private final ServerSocket mSocket;
        // Responses per URL query
        private final HashMap<String, MockResponse> mResponses = new HashMap<>();

        private static final class MockResponse {
            private final String mBody;
            private final String mMimetype;
            private final Map<String, String> mHeaders;

            MockResponse(String body, String mimetype, Map<String, String> headers) {
                this.mBody = body;
                this.mMimetype = mimetype;
                this.mHeaders = Collections.unmodifiableMap(new HashMap<>(headers));
            }
        }

        HttpServer() throws IOException {
            this(new ServerSocket());
        }

        private HttpServer(ServerSocket socket) {
            // 0 as port for picking a port automatically
            super(TEST_PORTAL_HOSTNAME, 0);
            mSocket = socket;
        }

        @Override
        public ServerSocketFactory getServerSocketFactory() {
            return () -> mSocket;
        }

        private String makeUrl(String query) {
            return new Uri.Builder()
                    .scheme("http")
                    .encodedAuthority(TEST_PORTAL_HOSTNAME + ":" + mSocket.getLocalPort())
                    .path(query)
                    .build()
                    .toString();
        }

        private void setResponseBody(String query, String body) {
            setResponse(query, body, NanoHTTPD.MIME_HTML, Collections.emptyMap());
        }

        private void setResponse(String query, String body, String mimetype,
                Map<String, String> headers) {
            mResponses.put(query, new MockResponse(body, mimetype, headers));
        }

        @Override
        public Response serve(IHTTPSession session) {
            final String path = URI.create(session.getUri()).getPath().substring(1);
            final MockResponse mockResponse = mResponses.get(path);
            if (mockResponse == null) {
                // Default response is a 404
                return super.serve(session);
            }

            final Response response = newFixedLengthResponse(Response.Status.OK,
                    mockResponse.mMimetype,
                    "<!doctype html>"
                    + "<html>"
                    + "<head><title>Test portal</title></head>"
                    + "<body>" + mockResponse.mBody + "</body>"
                    + "</html>");
            mockResponse.mHeaders.forEach(response::addHeader);
            return response;
        }
    }

    private HttpServer prepareTestDirectlyOpen(String linkIdDownload, String downloadQuery,
            String filename, String mimetype) throws Exception {
        // Setup the server with a single link on the portal page, leading to a download
        final HttpServer server = new HttpServer();
        server.setResponseBody(TEST_URL_QUERY,
                "<a id='" + linkIdDownload + "' href='" + downloadQuery + "'>Download</a>");
        server.setResponse(downloadQuery, "This is a test file", mimetype, Collections.singletonMap(
                "Content-Disposition", "attachment; filename=\"" + filename + "\""));
        server.start();

        ActivityScenario.launch(RequestDismissKeyguardActivity.class);
        initActivity(server.makeUrl(TEST_URL_QUERY));
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();
        return server;
    }

    private UiObject getUiSpinner() {
        final String resourceId = getInstrumentation().getContext().getResources()
                .getResourceName(R.id.download_in_progress);
        final UiSelector selector = new UiSelector().resourceId(resourceId);
        return UiDevice.getInstance(getInstrumentation()).findObject(selector);
    }

    private CompletableFuture<Boolean> initToastListener(String expectedMsg) {
        final CompletableFuture<Boolean> messageFuture = new CompletableFuture<>();
        getInstrumentation().getUiAutomation().setOnAccessibilityEventListener(
                new UiAutomation.OnAccessibilityEventListener() {
                    @Override
                    public void onAccessibilityEvent(AccessibilityEvent event) {
                        // Toast is contained in notification state change. Ignore other types.
                        if (event.getEventType() != TYPE_NOTIFICATION_STATE_CHANGED) {
                            return;
                        }

                        // Skip empty text events.
                        if (event.getText().size() == 0) return;

                        final String msg = (String) event.getText().get(0);
                        // The event class name in older SDK platform will be
                        // "android.widget.Toast$TN" instead of "android.widget.Toast".
                        if (event.getClassName().toString().contains(Toast.class.getName())
                                && expectedMsg.equals(msg)) {
                            messageFuture.complete(true);
                        }
                    }
                });
        return messageFuture;
    }

    @Test
    public void testDirectlyOpen_onCreateDeleteFile() throws Exception {
        final String linkIdDownload = "download";
        final HttpServer server = prepareTestDirectlyOpen(linkIdDownload, "dl",
                "test.wificonfig", TEST_WIFI_CONFIG_TYPE);
        final UiObject spinner = getUiSpinner();
        final File downloadPath = new File(getInstrumentation().getContext().getFilesDir(),
                CaptivePortalLoginActivity.FILE_PROVIDER_DOWNLOAD_PATH);

        assertNull(downloadPath.listFiles());

        onWebView().withElement(findElement(Locator.ID, linkIdDownload)).perform(webClick());
        assertTrue(spinner.waitForExists(TEST_TIMEOUT_MS));

        // The download file should be created.
        assertNotNull(downloadPath.listFiles());

        mActivityScenario.recreate();
        // OnCreate should clean the previous created files.
        assertNull(downloadPath.listFiles());
        server.stop();
    }

    @Test
    public void testDirectlyOpen_onDownloadAborted() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();
        final Uri mockFile = Uri.parse("content://mockdata");
        final String expectMsg = getInstrumentation().getContext().getString(
                R.string.file_too_large_cancel_download);
        final CompletableFuture<Boolean> toastFuture = initToastListener(expectMsg);

        mActivityScenario.onActivity(a -> a.mProgressCallback.onDownloadAborted(
                1, DOWNLOAD_ABORTED_REASON_FILE_TOO_LARGE));

        assertTrue(toastFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));

    }

    @Test
    public void testDirectlyOpen_taskCancelToast() throws Exception {
        final String linkIdDownload = "download";
        final String expectMsg = getInstrumentation().getContext().getString(
                R.string.cancel_pending_downloads);

        final HttpServer server = prepareTestDirectlyOpen(linkIdDownload, "dl",
                "test.wificonfig", TEST_WIFI_CONFIG_TYPE);
        onWebView().withElement(findElement(Locator.ID, linkIdDownload)).perform(webClick());

        final UiObject spinner = getUiSpinner();
        // Expect to see the spinner
        assertTrue(spinner.waitForExists(TEST_TIMEOUT_MS));
        final CompletableFuture<Boolean> toastFuture = initToastListener(expectMsg);
        mActivityScenario.onActivity(a -> a.cancelPendingTask());
        assertTrue(toastFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
        server.stop();
    }

    @Test
    public void testDirectlyOpen_cancelPendingTask() throws Exception {
        final String linkIdDownload = "download";
        final String downloadQuery = "dl";
        final String filename = "test.wificonfig";
        final String mimeType = TEST_WIFI_CONFIG_TYPE;
        final int requestId = 123;
        final HttpServer server = prepareTestDirectlyOpen(linkIdDownload, downloadQuery,
                filename, mimeType);

        final UiObject spinner = getUiSpinner();
        // Verify no spinner first.
        assertFalse(spinner.exists());
        doReturn(requestId).when(sDownloadServiceBinder)
                .requestDownload(any(), any(), any(), any(), any(), any(), eq(mimeType));
        onWebView().withElement(findElement(Locator.ID, linkIdDownload)).perform(webClick());
        // Expect to see the spinner
        assertTrue(spinner.waitForExists(TEST_TIMEOUT_MS));
        final ArgumentCaptor<Uri> outfileCaptor = ArgumentCaptor.forClass(Uri.class);
        verify(sDownloadServiceBinder).requestDownload(any(), any(),
                eq(server.makeUrl(downloadQuery)),
                eq(filename),
                outfileCaptor.capture(), any(), eq(mimeType));

        // Cancel pending task.
        mActivityScenario.onActivity(a -> a.cancelPendingTask());
        verify(sDownloadServiceBinder).cancelTask(anyInt());
        // Callback with target task should hide the spinner.
        mActivityScenario.onActivity(a -> a.mProgressCallback.onDownloadComplete(
                outfileCaptor.getValue(), mimeType, requestId, false));
        assertTrue(spinner.waitUntilGone(TEST_TIMEOUT_MS));

        server.stop();
    }

    @Test
    public void testDirectlyOpen_successfullyDownload() throws Exception {
        final String linkIdDownload = "download";
        final String mimeType = TEST_WIFI_CONFIG_TYPE;
        final String filename = "test.wificonfig";
        final Uri mockFile = Uri.parse("content://mockdata");
        final Uri otherFile = Uri.parse("content://otherdata");
        final int downloadId = 123;
        final int otherDownloadId = 456;
        final HttpServer server = prepareTestDirectlyOpen(linkIdDownload, "dl",
                filename, mimeType);
        doReturn(downloadId).when(sDownloadServiceBinder)
                .requestDownload(any(), any(), any(), any(), any(), any(), eq(mimeType));
        // Mock intents trying to actually install the profile
        Intents.intending(not(isInternal())).respondWith(new ActivityResult(RESULT_OK, null));

        final UiObject spinner = getUiSpinner();
        // Verify no spinner first.
        assertFalse(spinner.exists());

        onWebView().withElement(findElement(Locator.ID, linkIdDownload)).perform(webClick());

        // Expect to see the spinner
        assertTrue(spinner.waitForExists(TEST_TIMEOUT_MS));
        // File does not start a create file intent, i.e. no file picker
        assertEquals(0, Intents.getIntents().size());
        // Trigger callback with negative result with other undesired other download file.
        mActivityScenario.onActivity(a ->
                a.mProgressCallback.onDownloadComplete(otherFile, mimeType, otherDownloadId,
                        false));
        // Verify spinner is still visible and no intent to open the target file.
        assertTrue(spinner.exists());
        assertEquals(0, Intents.getIntents().size());

        // Trigger callback with positive result
        mActivityScenario.onActivity(a -> a.mProgressCallback.onDownloadComplete(
                mockFile, mimeType, downloadId, true));
        // Verify intent sent to open the target file
        final Intent sentIntent = Intents.getIntents().get(0);
        assertEquals(Intent.ACTION_VIEW, sentIntent.getAction());
        assertEquals(mimeType, sentIntent.getType());
        assertEquals(mockFile, sentIntent.getData());
        assertEquals(Intent.FLAG_GRANT_READ_URI_PERMISSION
                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION, sentIntent.getFlags());
        // Spinner should become invisible.
        assertTrue(spinner.waitUntilGone(TEST_TIMEOUT_MS));

        server.stop();
    }

    private LinkProperties makeLinkPropertiesWithPrivateDns() {
        final LinkProperties linkProperties = new LinkProperties();
        linkProperties.setUsePrivateDns(true);
        linkProperties.setPrivateDnsServerName(TEST_PRIVATE_DNS_SERVER);
        return linkProperties;
    }

    private MockCaptivePortal prepareCaptivePortalUsingCustomTabs(
            boolean isDelegateUidSetSuccessfully, final LinkProperties linkProperties)
            throws Exception {
        ActivityScenario.launch(RequestDismissKeyguardActivity.class);
        sIsMultiNetworkingSupportedByProvider = true;
        doReturn(linkProperties).when(sConnectivityManager).getLinkProperties(mNetwork);

        // Set up result stubbing for the CustomTabsIntent#launchUrl, this stub should be
        // initialized before starting CaptivePortalLoginActivity, otherwise, no activity
        // found to handle the CustomTabsIntent.
        Intents.init();
        intending(hasPackage(TEST_CUSTOM_TABS_PACKAGE_NAME))
                .respondWith(new ActivityResult(RESULT_OK, null));
        initActivity(TEST_URL);

        // Simulate the capabilities update callback with NET_CAPABILITY_CAPTIVE_PORTAL as
        // soon as the captive portal is detected. This ensures 'mIsPortal' is initialized
        // before the 'onNavigationEvent' callback is triggered.
        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_CAPTIVE_PORTAL, true);
        notifyValidatedChangedNotDone(nc);

        final MockCaptivePortal cp = getCaptivePortal();
        if (isDelegateUidSetSuccessfully) {
            mActivityScenario.onActivity(a -> cp.mDelegateUidReceiver.onResult(null));
        } else {
            mActivityScenario.onActivity(a -> cp.mDelegateUidReceiver.onError(
                    new ServiceSpecificException(OsConstants.EBUSY)));
        }
        return cp;
    }

    private void runCaptivePortalUsingCustomTabsTest(boolean isDelegateUidSetSuccessfully,
            final LinkProperties linkProperties) throws Exception {
        final MockCaptivePortal cp =
                prepareCaptivePortalUsingCustomTabs(isDelegateUidSetSuccessfully, linkProperties);

        final ArgumentCaptor<CustomTabsCallback> captor =
                ArgumentCaptor.forClass(CustomTabsCallback.class);
        verify(sMockCustomTabsClient, timeout(TEST_TIMEOUT_MS)).newSession(captor.capture());
        final CustomTabsCallback callback = captor.getValue();
        assertNotNull(callback);
        intended(allOf(
                hasExtra(Intent.EXTRA_REFERRER, Uri.parse("")),
                hasExtra(CustomTabsIntent.EXTRA_NETWORK, mNetwork),
                hasData(Uri.parse(TEST_URL))));

        // Send navigation start event, verify if the network will be reevaluated.
        callback.onNavigationEvent(NAVIGATION_STARTED, null /* extras */);
        assertEquals(1, cp.mReevaluateTimes);
        assertEquals(1, cp.mSetDelegateUidTimes);

        // Simulate a success login with NET_CAPABILITY_VALIDATED callback.
        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);

        // Send another navigation start event, verify that network won't be reevaluated
        // again after login.
        callback.onNavigationEvent(NAVIGATION_STARTED, null /* extras */);
        assertEquals(1, cp.mReevaluateTimes);
        assertEquals(1, cp.mSetDelegateUidTimes);
    }

    // only run this test on B and above when the private DNS is on, otherwise, Webview will
    // be launched rather than CCT.
    @Test
    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_privateDnsOn_bypassVpnOrPrivateDnsSuccess()
            throws Exception {
        final LinkProperties lp = makeLinkPropertiesWithPrivateDns();
        runCaptivePortalUsingCustomTabsTest(true /* isDelegateUidSetSuccessfully */, lp);
    }

    // only run this test on B and above when the private DNS is on, otherwise, Webview will
    // be launched rather than CCT.
    @Test
    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_privateDnsOn_bypassVpnOrPrivateDnsFailure()
            throws Exception {
        final LinkProperties lp = makeLinkPropertiesWithPrivateDns();
        runCaptivePortalUsingCustomTabsTest(false /* isDelegateUidSetSuccessfully */, lp);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_privateDnsOff_bypassVpnOrPrivateDnsSuccess()
            throws Exception {
        final LinkProperties lp = new LinkProperties();
        runCaptivePortalUsingCustomTabsTest(true /* isDelegateUidSetSuccessfully */, lp);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_privateDnsOff_bypassVpnOrPrivateDnsFailure()
            throws Exception {
        final LinkProperties lp = new LinkProperties();
        runCaptivePortalUsingCustomTabsTest(false /* isDelegateUidSetSuccessfully */, lp);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_closeAppWhenTabHiddenButParentStillResumed()
            throws Exception {
        final LinkProperties lp = new LinkProperties();
        prepareCaptivePortalUsingCustomTabs(true /* isDelegateUidSetSuccessfully */, lp);

        final ArgumentCaptor<CustomTabsCallback> captor =
                ArgumentCaptor.forClass(CustomTabsCallback.class);
        verify(sMockCustomTabsClient, timeout(TEST_TIMEOUT_MS)).newSession(captor.capture());
        final CustomTabsCallback callback = captor.getValue();
        assertNotNull(callback);

        ((EngagementSignalsCallback) callback).onSessionEnded(
                true /* didUserInteract */, null /* extras */);

        waitForDestroyedState();
        assertEquals(DESTROYED, mActivityScenario.getState());
    }

    // Only run this test on R+ and B-, because on B and above private DNS bypass is supported,
    // on R and below, OutcomeReceiver class is not available yet which is required for Custom
    // tab implementation.
    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_privateDnsOnWhileCustomTabActive()
            throws Exception {
        final LinkProperties lp = new LinkProperties();
        runCaptivePortalUsingCustomTabsTest(true /* isDelegateUidSetSuccessfully */, lp);

        // Simulate to enable the private DNS while the custom tab is active.
        lp.setUsePrivateDns(true);
        lp.setPrivateDnsServerName("strict.example.com");
        notifyLinkPropertiesChanged(lp);

        // Verify the done(Result.DISMISSED) will be called.
        final MockCaptivePortal cp = getCaptivePortal();
        assertEquals(cp.mDismissTimes, 1);
        assertEquals(cp.mIgnoreTimes, 0);
        assertEquals(cp.mUseTimes, 0);
    }

    private void verifyWebViewInitialization() {
        verify(sConnectivityManager).bindProcessToNetwork(any());
        verify(sMockCustomTabsClient, never()).newSession(any());
        mActivityScenario.onActivity(activity ->
                assertNotNull(activity.findViewById(R.id.webview)));
    }

    private void verifyUsingWebViewRatherThanCustomTabs() {
        Intents.init();
        intending(hasPackage(TEST_CUSTOM_TABS_PACKAGE_NAME))
                .respondWith(new ActivityResult(RESULT_OK, null));
        initActivity(TEST_URL);
        verifyWebViewInitialization();
    }

    @Test
    @IgnoreAfter(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_setDelegateUidNotSupported_R() throws Exception {
        sIsMultiNetworkingSupportedByProvider = true;
        verifyUsingWebViewRatherThanCustomTabs();
    }

    @Test
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = false)
    public void testCaptivePortalUsingCustomTabs_flagOff() throws Exception {
        sIsMultiNetworkingSupportedByProvider = true;
        verifyUsingWebViewRatherThanCustomTabs();
    }

    @Test
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_multiNetworkNotSupported() throws Exception {
        sIsMultiNetworkingSupportedByProvider = false;
        final LinkProperties linkProperties = new LinkProperties();
        doReturn(linkProperties).when(sConnectivityManager).getLinkProperties(mNetwork);
        verifyUsingWebViewRatherThanCustomTabs();
    }

    @Test
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalUsingCustomTabs_nullLinkProperties() throws Exception {
        sIsMultiNetworkingSupportedByProvider = true;
        doReturn(null).when(sConnectivityManager).getLinkProperties(mNetwork);
        verifyUsingWebViewRatherThanCustomTabs();
    }

    private void findMoreButtonAndClickTheMenuItem(final int menuItemIndex) throws Exception {
        final UiDevice device = UiDevice.getInstance(getInstrumentation());
        final UiSelector moreButtonSelector =
                new UiSelector().descriptionContains("Customize and control");
        final UiObject moreButton = device.findObject(moreButtonSelector);
        // TODO: investigate why the more button is not found on certain device. see b/428907299.
        assumeTrue("The Custom Tab more button was not found.",
                moreButton.waitForExists(TEST_TIMEOUT_MS));
        moreButton.click();

        device.waitForIdle();

        // Choosing the menu item by index is not a great solution because there is no
        // guarantee that Chrome will never change the order of the menus. This is only
        // the index of the menu added by the captive portal login activity and this test
        // assumes they are all at the top, which is the current chrome behavior.
        final UiSelector menuItemSelector =
                new UiSelector().className("android.widget.TextView").instance(menuItemIndex);
        final UiObject menuItem = device.findObject(menuItemSelector);
        assumeTrue("The expected menu item was not found.",
                menuItem.waitForExists(TEST_TIMEOUT_MS));
        menuItem.click();

        device.waitForIdle();
    }

    private MockCaptivePortal runCustomTabsMenuItemsTest(final int menuItemIndex) throws Exception {
        // Turn on the screen and dismiss the keyguard, allow UI automation to select the button.
        ActivityScenario.launch(RequestDismissKeyguardActivity.class);

        final MockCaptivePortal cp = prepareCaptivePortalUsingCustomTabs(
                true /* isDelegateUidSetSuccessfully */, new LinkProperties());
        findMoreButtonAndClickTheMenuItem(menuItemIndex);
        return cp;
    }

    private static boolean isChromeInstalled() {
        try {
            final Context context = getInstrumentation().getContext();
            context.getPackageManager().getPackageInfo("com.android.chrome", 0);
            return true;
        } catch (NameNotFoundException e) {
            return false;
        }
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCustomTabsMenuItems_clickDoNotUseThisNetwork() throws Exception {
        // This test requires to launch a custom tab provider Chrome and depends on the
        // specific UI of chrome. For those platforms without chrome browser installed
        // such as aosp platforms, then skip this test.
        assumeTrue(isChromeInstalled());
        final MockCaptivePortal cp =
                runCustomTabsMenuItemsTest(CUSTOM_TAB_MENU_ITEM_DO_NOT_USE_THIS_NETWORK);
        assertEquals(1, cp.mSetDelegateUidTimes);
        assertEquals(1, cp.mIgnoreTimes);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCustomTabsMenuItems_clickUseThisNetwork() throws Exception {
        // This test requires to launch a custom tab provider Chrome and depends on the
        // specific UI of chrome. For those platforms without chrome browser installed
        // such as aosp platforms, then skip this test.
        assumeTrue(isChromeInstalled());
        final MockCaptivePortal cp =
                runCustomTabsMenuItemsTest(CUSTOM_TAB_MENU_ITEM_USE_THIS_NETWORK);
        assertEquals(1, cp.mSetDelegateUidTimes);
        assertEquals(1, cp.mUseTimes);
    }

    private void verifyIntentExtrasForForceWebview() {
        assertEquals(mNetwork, sIntentExtrasForceWebview.getParcelable(EXTRA_NETWORK));
        assertEquals(TEST_URL, sIntentExtrasForceWebview.getString(EXTRA_CAPTIVE_PORTAL_URL));
        assertEquals(TEST_USERAGENT,
                sIntentExtrasForceWebview.getString(EXTRA_CAPTIVE_PORTAL_USER_AGENT));
        assertEquals(true, sIntentExtrasForceWebview.getBoolean(EXTRA_USE_CLASSIC_VIEW));
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCustomTabsMenuItems_clickUseOldInterface() throws Exception {
        // This test requires to launch a custom tab provider Chrome and depends on the
        // specific UI of chrome. For those platforms without chrome browser installed
        // such as aosp platforms, then skip this test.
        assumeTrue(isChromeInstalled());
        runCustomTabsMenuItemsTest(CUSTOM_TAB_MENU_ITEM_USE_CLASSIC_VIEW);
        getInstrumentation().waitForIdleSync();
        assertEquals(mNetwork, sNetworkForceWebview);
        verifyIntentExtrasForForceWebview();
        Intents.release();

        clearInvocations(sConnectivityManager);
        clearInvocations(sMockCustomTabsClient);

        // Simulate launching a new CaptivePortalLoginActivity with intent including
        // EXTRA_USE_CLASSIC_VIEW, which is set when menu item "Use classic view" is
        // clicked, and verify the Webview should be initialized instead of CCT.
        Intents.init();
        initActivity(TEST_URL, true /* useClassicView */);
        verifyWebViewInitialization();
    }

    private void verifyCaptivePortalLoginMetrics(final boolean expectWebview, final int result,
            final int reason) {
        // UID isn't set in Webview case.
        if (!expectWebview) {
            verify(sMockCaptivePortalLoginMetrics).setUid(eq(TEST_CUSTOM_TABS_PROVIDER_UID));
        } else {
            verify(sMockCaptivePortalLoginMetrics, never()).setUid(anyInt());
        }
        verify(sMockCaptivePortalLoginMetrics).setPortalResult(eq(result));
        if (reason != CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_UNKNOWN) {
            verify(sMockCaptivePortalLoginMetrics).setReason(eq(reason));
        }
    }

    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = false)
    public void testCaptivePortalMetrics_useWebview_dismissed() throws Exception {
        initActivity(TEST_URL);
        // Initialize intent capturing after launching the activity to avoid capturing extra
        // intents.
        Intents.init();

        // NetworkCapabilities updates w/ NET_CAPABILITY_VALIDATED.
        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(true /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_FEATURE_NOT_ENABLED);
    }

    @Test
    @IgnoreUpTo(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalMetrics_useCustomTabs_dismissed() throws Exception {
        final LinkProperties lp = new LinkProperties();
        runCaptivePortalUsingCustomTabsTest(true /* isDelegateUidSetSuccessfully */, lp);

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(false /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_UNKNOWN);

        // Simulate to bring the captive portal activity back to foreground by launching a
        // new activity with the same EXTRA_CAPTIVE_PORTAL.
        final Context ctx = getInstrumentation().getContext();
        final MockCaptivePortal cp = getCaptivePortal();
        final Intent intent = makeIntent(ctx, TEST_URL, false /* useClassicView */, cp);
        mActivityScenario = ActivityScenario.launch(intent);
        getInstrumentation().waitForIdleSync();

        // After bringing the activity to the foreground, trigger another NET_CAPABILITY_VALIDATED
        // callback and verify that the dismiss logic does not run a second time.
        notifyCapabilitiesChanged(nc);
        assertEquals(cp.mDismissTimes, 1); // the portal has been dismissed once before.
        assertEquals(cp.mIgnoreTimes, 0);
        assertEquals(cp.mUseTimes, 0);
    }

    @Test
    @IgnoreAfter(Build.VERSION_CODES.R)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalMetrics_fallbackToWebview_R() throws Exception {
        sIsMultiNetworkingSupportedByProvider = true;
        verifyUsingWebViewRatherThanCustomTabs();

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(true /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_RUNNING_ANDROID_R);
    }

    @Test
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalMetrics_fallbackToWebview_notSupportMultiNetwork()
            throws Exception {
        sIsMultiNetworkingSupportedByProvider = false;
        final LinkProperties linkProperties = new LinkProperties();
        doReturn(linkProperties).when(sConnectivityManager).getLinkProperties(mNetwork);
        verifyUsingWebViewRatherThanCustomTabs();

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(true /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_NOT_SUPPORT_MULTI_NETWORK);
    }

    @Test
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalMetrics_fallbackToWebview_useClassicView()
            throws Exception {
        initActivity(TEST_URL, true /* useClassicView */);
        Intents.init();
        verifyWebViewInitialization();

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(true /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_USE_CLASSIC_VIEW);
    }

    // only run this test on V and below when the private DNS is on, otherwise, metrics won't be
    // logged.
    @Test
    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    @FeatureFlag(name = CAPTIVE_PORTAL_CUSTOM_TABS, enabled = true)
    public void testCaptivePortalMetrics_fallbackToWebview_privateDnsOnVAndBelow()
            throws Exception {
        sIsMultiNetworkingSupportedByProvider = true;
        final LinkProperties linkProperties = makeLinkPropertiesWithPrivateDns();
        doReturn(linkProperties).when(sConnectivityManager).getLinkProperties(mNetwork);
        verifyUsingWebViewRatherThanCustomTabs();

        final NetworkCapabilities nc = new NetworkCapabilities();
        nc.setCapability(NET_CAPABILITY_VALIDATED, true);
        notifyValidatedChangedAndDismissed(nc);
        verifyCaptivePortalLoginMetrics(true /* expectWebview */,
                CAPTIVE_PORTAL_LOGIN_REPORTED__PORTAL_RESULT__CAPTIVE_PORTAL_RESULT_SUCCESS,
                CAPTIVE_PORTAL_LOGIN_REPORTED__REASON__REASON_PRIVATE_DNS_ENABLED_V_AND_BELOW);
    }
}
