chromium/android_webview/javatests/src/org/chromium/android_webview/test/SafeModeTest.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview.test;

import static org.hamcrest.Matchers.greaterThan;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.EITHER_PROCESS;

import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.RemoteException;

import androidx.annotation.NonNull;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;

import org.chromium.android_webview.BrowserSafeModeActionList;
import org.chromium.android_webview.common.SafeModeAction;
import org.chromium.android_webview.common.SafeModeActionIds;
import org.chromium.android_webview.common.SafeModeController;
import org.chromium.android_webview.common.VariationsFastFetchModeUtils;
import org.chromium.android_webview.common.services.ISafeModeService;
import org.chromium.android_webview.common.variations.VariationsUtils;
import org.chromium.android_webview.services.AwVariationsSeedFetcher;
import org.chromium.android_webview.services.NonEmbeddedFastVariationsSeedSafeModeAction;
import org.chromium.android_webview.services.NonEmbeddedSafeModeAction;
import org.chromium.android_webview.services.NonEmbeddedSafeModeActionsSetupCleanup;
import org.chromium.android_webview.services.SafeModeService;
import org.chromium.android_webview.services.SafeModeService.TrustedPackage;
import org.chromium.android_webview.test.VariationsSeedLoaderTest.TestLoader;
import org.chromium.android_webview.test.VariationsSeedLoaderTest.TestLoaderResult;
import org.chromium.android_webview.test.services.ServiceConnectionHelper;
import org.chromium.android_webview.test.util.VariationsTestUtils;
import org.chromium.android_webview.variations.FastVariationsSeedSafeModeAction;
import org.chromium.android_webview.variations.VariationsSeedSafeModeAction;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.PathUtils;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.build.BuildConfig;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.variations.firstrun.VariationsSeedFetcher;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/** Test WebView SafeMode. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@OnlyRunIn(EITHER_PROCESS) // These tests don't use the renderer process
public class SafeModeTest extends AwParameterizedTest {
    // The package name of the test shell. This is acting both as the client app and the WebView
    // provider.
    public static final String TEST_WEBVIEW_PACKAGE_NAME = "org.chromium.android_webview.shell";

    // This is the actual certificate hash we use to sign webview_instrumentation_apk (Signer #1
    // certificate SHA-256 digest). This can be obtained by running:
    // $ out/Default/bin/webview_instrumentation_apk print-certs
    private static final byte[] TEST_WEBVIEW_CERT_HASH =
            new byte[] {
                (byte) 0x32,
                (byte) 0xa2,
                (byte) 0xfc,
                (byte) 0x74,
                (byte) 0xd7,
                (byte) 0x31,
                (byte) 0x10,
                (byte) 0x58,
                (byte) 0x59,
                (byte) 0xe5,
                (byte) 0xa8,
                (byte) 0x5d,
                (byte) 0xf1,
                (byte) 0x6d,
                (byte) 0x95,
                (byte) 0xf1,
                (byte) 0x02,
                (byte) 0xd8,
                (byte) 0x5b,
                (byte) 0x22,
                (byte) 0x09,
                (byte) 0x9b,
                (byte) 0x80,
                (byte) 0x64,
                (byte) 0xc5,
                (byte) 0xd8,
                (byte) 0x91,
                (byte) 0x5c,
                (byte) 0x61,
                (byte) 0xda,
                (byte) 0xd1,
                (byte) 0xe0
            };

    // Arbitrary sha256 digest which does not match TEST_WEBVIEW_PACKAGE_NAME's certificate.
    private static final byte[] FAKE_CERT_HASH =
            new byte[] {
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF,
                (byte) 0xFF
            };

    private static final String SAFEMODE_ACTION_NAME = "some_action_name";

    private AtomicInteger mTestSafeModeActionExecutionCounter;

    private TestJobScheduler mScheduler = new TestJobScheduler();
    private TestVariationsSeedFetcher mDownloader = new TestVariationsSeedFetcher();
    private static final int HTTP_NOT_FOUND = 404;
    private static final int HTTP_NOT_MODIFIED = 304;
    private static final int JOB_ID = TaskIds.WEBVIEW_VARIATIONS_SEED_FETCH_JOB_ID;

    // A test JobScheduler which only holds one job, and never does anything with it.
    private class TestJobScheduler extends JobScheduler {
        public JobInfo mJob;
        public QueueContainer mQueueContainer = new QueueContainer();

        public void clear() {
            mJob = null;
        }

        public void assertScheduled() {
            Assert.assertNotNull("No job scheduled", mJob);
        }

        public void assertNotScheduled() {
            Assert.assertNull("Job should not have been scheduled", mJob);
        }

        public void assertScheduled(int jobId) {
            Assert.assertEquals("No job scheduled", mJob.getId(), jobId);
        }

        @Override
        public void cancel(int jobId) {
            if (mJob == null) return;
            if (mJob.getId() == jobId) mJob = null;
        }

        @Override
        public void cancelAll() {
            throw new UnsupportedOperationException();
        }

        @Override
        public int enqueue(JobInfo job, JobWorkItem work) {
            throw new UnsupportedOperationException();
        }

        @Override
        public List<JobInfo> getAllPendingJobs() {
            ArrayList<JobInfo> list = new ArrayList<>();
            if (mJob != null) list.add(mJob);
            return list;
        }

        @Override
        public JobInfo getPendingJob(int jobId) {
            if (mJob != null && mJob.getId() == jobId) return mJob;
            return null;
        }

        @Override
        public int schedule(JobInfo job) {
            Assert.assertTrue("Job scheduled with wrong ID", JOB_ID == job.getId());
            mJob = job;
            mQueueContainer.notifyCalled(job);
            return JobScheduler.RESULT_SUCCESS;
        }

        public JobInfo waitForMessageCallback() throws Exception {
            return mQueueContainer.waitForMessageCallback();
        }
    }

    private static class QueueContainer {
        private LinkedBlockingQueue<JobInfo> mQueue = new LinkedBlockingQueue<>();

        public void notifyCalled(JobInfo job) {
            try {
                mQueue.add(job);
            } catch (IllegalStateException e) {
                // We expect this add operation will always succeed since the default capacity of
                // the queue is Integer.MAX_VALUE.
            }
        }

        public JobInfo waitForMessageCallback() throws Exception {
            return AwActivityTestRule.waitForNextQueueElement(mQueue);
        }

        public boolean isQueueEmpty() {
            return mQueue.isEmpty();
        }
    }

    // A test VariationsSeedFetcher which doesn't actually download seeds, but verifies the request
    // parameters.
    private class TestVariationsSeedFetcher extends VariationsSeedFetcher {
        private static final String SAVED_VARIATIONS_SEED_SERIAL_NUMBER = "savedSerialNumber";

        public int fetchResult;

        @Override
        public SeedFetchInfo downloadContent(
                VariationsSeedFetcher.SeedFetchParameters params, SeedInfo currInfo) {
            Assert.assertEquals(
                    VariationsSeedFetcher.VariationsPlatform.ANDROID_WEBVIEW, params.getPlatform());
            Assert.assertThat(Integer.parseInt(params.getMilestone()), greaterThan(0));

            SeedFetchInfo fetchInfo = new SeedFetchInfo();
            // Pretend the servers-side |serialNumber| equals |SAVED_VARIATIONS_SEED_SERIAL_NUMBER|
            // and return |HTTP_NOT_MODIFIED|
            if (currInfo != null
                    && currInfo.getParsedVariationsSeed()
                            .getSerialNumber()
                            .equals(SAVED_VARIATIONS_SEED_SERIAL_NUMBER)) {
                fetchInfo.seedInfo = currInfo;
                fetchInfo.seedInfo.date = getDateTime().newDate().getTime();
                fetchInfo.seedFetchResult = HTTP_NOT_MODIFIED;
            } else {
                fetchInfo.seedFetchResult = fetchResult;
            }
            return fetchInfo;
        }
    }

    @Rule public AwActivityTestRule mActivityTestRule;

    public SafeModeTest(AwSettingsMutation param) {
        this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
    }

    @Before
    public void setUp() throws Throwable {
        mTestSafeModeActionExecutionCounter = new AtomicInteger(0);
        AwVariationsSeedFetcher.setMocks(mScheduler, mDownloader);
        VariationsTestUtils.deleteSeeds();
    }

    @After
    public void tearDown() throws Throwable {
        // Reset component state back to the default.
        final Context context = ContextUtils.getApplicationContext();
        ComponentName safeModeComponent =
                new ComponentName(
                        TEST_WEBVIEW_PACKAGE_NAME, SafeModeController.SAFE_MODE_STATE_COMPONENT);
        context.getPackageManager()
                .setComponentEnabledSetting(
                        safeModeComponent,
                        PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                        PackageManager.DONT_KILL_APP);

        SafeModeController.getInstance().unregisterActionsForTesting();

        SafeModeService.clearSharedPrefsForTesting();
        mScheduler.cancel(JOB_ID);
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_disabledByDefault() throws Throwable {
        Assert.assertFalse(
                "SafeMode should be off by default",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_readComponentState() throws Throwable {
        // Enable the component directly.
        final Context context = ContextUtils.getApplicationContext();
        ComponentName safeModeComponent =
                new ComponentName(
                        TEST_WEBVIEW_PACKAGE_NAME, SafeModeController.SAFE_MODE_STATE_COMPONENT);
        context.getPackageManager()
                .setComponentEnabledSetting(
                        safeModeComponent,
                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                        PackageManager.DONT_KILL_APP);

        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_enableWithMethod() throws Throwable {
        SafeModeService.setSafeMode(Arrays.asList(SAFEMODE_ACTION_NAME));
        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_disableWithMethod() throws Throwable {
        SafeModeService.setSafeMode(Arrays.asList(SAFEMODE_ACTION_NAME));
        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));

        SafeModeService.setSafeMode(Arrays.asList());
        Assert.assertFalse(
                "SafeMode should be re-disabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_enableWithService() throws Throwable {
        setSafeMode(Arrays.asList(SAFEMODE_ACTION_NAME));

        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testSafeModeState_disableWithService() throws Throwable {
        setSafeMode(Arrays.asList(SAFEMODE_ACTION_NAME));

        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));

        setSafeMode(Arrays.asList());

        Assert.assertFalse(
                "SafeMode should be re-disabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_disabled() throws Throwable {
        Assert.assertEquals(
                "Querying the ContentProvider should yield empty set when SafeMode is disabled",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_singleAction() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        setSafeMode(Arrays.asList(variationsActionId));

        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "Querying the ContentProvider should yield the action we set",
                asSet(variationsActionId),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_multipleActions() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        setSafeMode(Arrays.asList(SAFEMODE_ACTION_NAME, variationsActionId));

        Assert.assertTrue(
                "SafeMode should be enabled",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "Querying the ContentProvider should yield the action we set",
                asSet(SAFEMODE_ACTION_NAME, variationsActionId),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_autoDisableAfter30Days() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        final long initialStartTimeMs = 12345L;
        SafeModeService.setClockForTesting(
                () -> {
                    return initialStartTimeMs;
                });
        setSafeMode(Arrays.asList(variationsActionId));

        final long beforeTimeLimitMs =
                initialStartTimeMs + SafeModeService.SAFE_MODE_ENABLED_TIME_LIMIT_MS - 1L;
        SafeModeService.setClockForTesting(
                () -> {
                    return beforeTimeLimitMs;
                });

        Assert.assertTrue(
                "SafeMode should be enabled (before timeout)",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "Querying the ContentProvider should yield the action we set",
                asSet(variationsActionId),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));

        final long afterTimeLimitMs =
                initialStartTimeMs + SafeModeService.SAFE_MODE_ENABLED_TIME_LIMIT_MS;
        SafeModeService.setClockForTesting(
                () -> {
                    return afterTimeLimitMs;
                });

        Assert.assertTrue(
                "SafeMode should be enabled until querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "ContentProvider should return empty set after timeout",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertFalse(
                "SafeMode should be disabled after querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_autoDisableIfTimestampInFuture() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        final long initialStartTimeMs = 12345L;
        SafeModeService.setClockForTesting(
                () -> {
                    return initialStartTimeMs;
                });
        setSafeMode(Arrays.asList(variationsActionId));

        // If the user manually sets their clock backward in time, then the time delta will be
        // negative. This case should also be treated as expired.
        final long queryTime = initialStartTimeMs - 1L;
        SafeModeService.setClockForTesting(
                () -> {
                    return queryTime;
                });

        Assert.assertTrue(
                "SafeMode should be enabled until querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "ContentProvider should return empty set after timeout",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertFalse(
                "SafeMode should be disabled after querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_extendTimeoutWithDuplicateConfig() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        final long initialStartTimeMs = 12345L;
        SafeModeService.setClockForTesting(
                () -> {
                    return initialStartTimeMs;
                });
        setSafeMode(Arrays.asList(variationsActionId));

        // Send a duplicate config after 1 day to extend the SafeMode timeout for another 30 days.
        final long duplicateConfigTimeMs = initialStartTimeMs + TimeUnit.DAYS.toMillis(1);
        SafeModeService.setClockForTesting(
                () -> {
                    return duplicateConfigTimeMs;
                });
        setSafeMode(Arrays.asList(variationsActionId));

        // 30 days after the original timeout
        final long firstTimeLimitMs =
                initialStartTimeMs + SafeModeService.SAFE_MODE_ENABLED_TIME_LIMIT_MS;
        SafeModeService.setClockForTesting(
                () -> {
                    return firstTimeLimitMs;
                });

        Assert.assertEquals(
                "Querying the ContentProvider should yield the action we set (timeout extended)",
                asSet(variationsActionId),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));

        final long secondTimeLimitMs =
                duplicateConfigTimeMs + SafeModeService.SAFE_MODE_ENABLED_TIME_LIMIT_MS;
        SafeModeService.setClockForTesting(
                () -> {
                    return secondTimeLimitMs;
                });

        Assert.assertEquals(
                "ContentProvider should return empty set after timeout",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_autoDisableIfMissingTimestamp() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        setSafeMode(Arrays.asList(variationsActionId));

        // If for some reason LAST_MODIFIED_TIME_KEY is unexpectedly missing, SafeMode should
        // disable itself.
        SafeModeService.removeSharedPrefKeyForTesting(SafeModeService.LAST_MODIFIED_TIME_KEY);

        Assert.assertTrue(
                "SafeMode should be enabled until querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "ContentProvider should return empty set after timeout",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertFalse(
                "SafeMode should be disabled after querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_autoDisableIfMissingActions() throws Throwable {
        final String variationsActionId = new VariationsSeedSafeModeAction().getId();
        setSafeMode(Arrays.asList(variationsActionId));

        // If for some reason SAFEMODE_ACTIONS_KEY is unexpectedly missing (or the empty set),
        // SafeMode should disable itself.
        SafeModeService.removeSharedPrefKeyForTesting(SafeModeService.SAFEMODE_ACTIONS_KEY);

        Assert.assertTrue(
                "SafeMode should be enabled until querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertEquals(
                "ContentProvider should return empty set after timeout",
                asSet(),
                SafeModeController.getInstance().queryActions(TEST_WEBVIEW_PACKAGE_NAME));
        Assert.assertFalse(
                "SafeMode should be disabled after querying ContentProvider",
                SafeModeController.getInstance().isSafeModeEnabled(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testQueryActions_emptyAction() throws Throwable {
        final String invalidWebViewPackageName = "org.chromium.android_webview.test";

        Assert.assertFalse(
                "SafeMode should be disabled",
                SafeModeController.getInstance().isSafeModeEnabled(invalidWebViewPackageName));
        Assert.assertEquals(
                "ContentProvider should return empty set when cursor is null",
                asSet(),
                SafeModeController.getInstance().queryActions(invalidWebViewPackageName));
    }

    private class TestSafeModeAction implements SafeModeAction {
        private int mCallCount;
        private int mExecutionOrder;
        private final String mId;
        private final boolean mSuccess;

        TestSafeModeAction(String id) {
            this(id, true);
        }

        TestSafeModeAction(String id, boolean success) {
            mId = id;
            mSuccess = success;
        }

        @Override
        @NonNull
        public String getId() {
            return mId;
        }

        @Override
        public boolean execute() {
            mCallCount++;
            mExecutionOrder = mTestSafeModeActionExecutionCounter.incrementAndGet();
            return mSuccess;
        }

        public int getCallCount() {
            return mCallCount;
        }

        public int getExecutionOrder() {
            return mExecutionOrder;
        }
    }

    private class TestNonEmbeddedSafeModeAction implements NonEmbeddedSafeModeAction {
        private int mActivatedCount;
        private int mDeactivatedCount;
        private final String mId;
        private final boolean mSuccess;

        TestNonEmbeddedSafeModeAction(String id) {
            this(id, true);
        }

        TestNonEmbeddedSafeModeAction(String id, boolean success) {
            mId = id;
            mSuccess = success;
        }

        @Override
        @NonNull
        public String getId() {
            return mId;
        }

        @Override
        public boolean onActivate() {
            mActivatedCount++;
            return mSuccess;
        }

        @Override
        public boolean onDeactivate() {
            mDeactivatedCount++;
            return mSuccess;
        }

        public int getActivatedCallCount() {
            return mActivatedCount;
        }

        public int getDeactivatedCallCount() {
            return mDeactivatedCount;
        }
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_cannotRegisterActionsTwice() throws Throwable {
        TestSafeModeAction testAction1 = new TestSafeModeAction("test1");
        TestSafeModeAction testAction2 = new TestSafeModeAction("test2");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction1});
        try {
            SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction2});
            Assert.fail(
                    "SafeModeController should have thrown an exception when "
                            + "re-registering actions");
        } catch (IllegalStateException e) {
            // Expected
        }
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_cannotRegisterDuplicateActionId() throws Throwable {
        Assume.assumeTrue(
                "This behavior is only in debug builds for performance reasons",
                BuildConfig.ENABLE_ASSERTS);
        TestSafeModeAction testAction1 = new TestSafeModeAction("test1");
        TestSafeModeAction testAction2 = new TestSafeModeAction("test1");
        try {
            SafeModeController.getInstance()
                    .registerActions(new SafeModeAction[] {testAction1, testAction2});
            Assert.fail(
                    "SafeModeController should have thrown an exception for " + "a duplicate ID");
        } catch (IllegalArgumentException e) {
            // Expected
        }
    }

    private static <T> Set<T> asSet(T... values) {
        Set<T> set = new HashSet<>();
        for (T value : values) {
            set.add(value);
        }
        return set;
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_mustRegisterBeforeExecuting() throws Throwable {
        try {
            Set<String> actions = asSet("test");
            SafeModeController.getInstance().executeActions(actions);
            Assert.fail(
                    "SafeModeController should have thrown an exception when "
                            + "executing without registering");
        } catch (IllegalStateException e) {
            // Expected
        }
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_executesRegisteredAction() throws Throwable {
        TestSafeModeAction testAction = new TestSafeModeAction("test");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});

        Set<String> actions = asSet("test");
        SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                "TestSafeModeAction should have been executed exactly 1 time",
                1,
                testAction.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_doesNotExecuteUnregisteredActions() throws Throwable {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.WebView.SafeMode.ExecutionResult",
                        SafeModeController.SafeModeExecutionResult.ACTION_UNKNOWN);
        TestSafeModeAction testAction = new TestSafeModeAction("test");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});

        Set<String> actions = asSet(testAction.getId(), "unregistered1", "unregistered2");
        @SafeModeController.SafeModeExecutionResult
        int success = SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                "TestSafeModeAction should have been executed exactly 1 time",
                1,
                testAction.getCallCount());
        Assert.assertEquals(
                "Overall status should be unknown if at least one action is unrecognized and no"
                        + " actions failed",
                success,
                SafeModeController.SafeModeExecutionResult.ACTION_UNKNOWN);
        histogramExpectation.assertExpected("Unregistered safemode actions should be logged");
        // If we got this far without crashing, we assume SafeModeController correctly ignored the
        // unregistered actions.
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_testStatusHierarchyEnforcedCorrectly() throws Throwable {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.WebView.SafeMode.ExecutionResult",
                        SafeModeController.SafeModeExecutionResult.ACTION_FAILED);
        TestSafeModeAction testActionFailed = new TestSafeModeAction("testFail", false);
        TestSafeModeAction testActionSuccess = new TestSafeModeAction("testSuccess");
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testActionSuccess, testActionFailed});

        // The possible execution statuses include SUCCESS, ACTION_FAILED, and ACTION_UNKNOWN.
        // The precedence is ACTION_FAILED, ACTION_UNKNOWN, and then SUCCESS in descending order.
        Set<String> actions =
                asSet(
                        testActionSuccess.getId(),
                        testActionFailed.getId(),
                        "unregistered1",
                        "unregistered2");
        @SafeModeController.SafeModeExecutionResult
        int success = SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                testActionFailed.getId() + " should have been executed exactly 1 time",
                1,
                testActionFailed.getCallCount());
        Assert.assertEquals(
                testActionSuccess.getId() + " should have been executed exactly 1 time",
                1,
                testActionSuccess.getCallCount());
        Assert.assertEquals(
                "Overall status should be failure if at least one"
                        + " action is unrecognized and at least one action is a failure",
                success,
                SafeModeController.SafeModeExecutionResult.ACTION_FAILED);
        histogramExpectation.assertExpected("Failed safemode actions should be logged");
        // If we got this far without crashing, we assume SafeModeController correctly ignored the
        // unregistered actions.
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_onlyExecutesSpecifiedActions() throws Throwable {
        TestSafeModeAction testAction1 = new TestSafeModeAction("test1");
        TestSafeModeAction testAction2 = new TestSafeModeAction("test2");
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction1, testAction2});

        Set<String> actions = asSet("test1");
        SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                "testAction1 should have been executed exactly 1 time",
                1,
                testAction1.getCallCount());
        Assert.assertEquals(
                "testAction2 should not have been executed", 0, testAction2.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_executesActionsInOrder() throws Throwable {
        TestSafeModeAction testAction1 = new TestSafeModeAction("test1");
        TestSafeModeAction testAction2 = new TestSafeModeAction("test2");
        TestSafeModeAction testAction3 = new TestSafeModeAction("test3");

        Set<String> actions = asSet(testAction1.getId(), testAction2.getId(), testAction3.getId());

        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction1, testAction2, testAction3});
        SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                "testAction1 should be executed first", 1, testAction1.getExecutionOrder());
        Assert.assertEquals(
                "testAction2 should be executed second", 2, testAction2.getExecutionOrder());
        Assert.assertEquals(
                "testAction3 should be executed third", 3, testAction3.getExecutionOrder());

        // Unregister and re-register in the opposite order. Verify that they're executed in the new
        // registration order.
        SafeModeController.getInstance().unregisterActionsForTesting();
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction3, testAction2, testAction1});
        SafeModeController.getInstance().executeActions(actions);
        Assert.assertEquals(
                "testAction3 should be executed first the next time",
                4,
                testAction3.getExecutionOrder());
        Assert.assertEquals(
                "testAction2 should be executed second the next time",
                5,
                testAction2.getExecutionOrder());
        Assert.assertEquals(
                "testAction1 should be executed third the next time",
                6,
                testAction1.getExecutionOrder());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_overallSuccessStatus() throws Throwable {
        HistogramWatcher histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.WebView.SafeMode.ExecutionResult",
                        SafeModeController.SafeModeExecutionResult.SUCCESS);
        TestSafeModeAction successAction1 = new TestSafeModeAction("successAction1");
        TestSafeModeAction successAction2 = new TestSafeModeAction("successAction2");
        Set<String> allSuccessful = asSet(successAction1.getId(), successAction2.getId());
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {successAction1, successAction2});
        @SafeModeController.SafeModeExecutionResult
        int success = SafeModeController.getInstance().executeActions(allSuccessful);
        Assert.assertEquals(
                "Overall status should be successful if all actions are successful",
                success,
                SafeModeController.SafeModeExecutionResult.SUCCESS);
        histogramExpectation.assertExpected(
                "Overall status should be successful if all actions are successful");
        Assert.assertEquals(
                "successAction1 should have been executed exactly 1 time",
                1,
                successAction1.getCallCount());
        Assert.assertEquals(
                "successAction2 should have been executed exactly 1 time",
                1,
                successAction2.getCallCount());

        // Register a new set of actions where at least one indicates failure.
        histogramExpectation =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.WebView.SafeMode.ExecutionResult",
                        SafeModeController.SafeModeExecutionResult.ACTION_FAILED);
        SafeModeController.getInstance().unregisterActionsForTesting();
        TestSafeModeAction failAction = new TestSafeModeAction("failAction", false);
        Set<String> oneFailure =
                asSet(successAction1.getId(), failAction.getId(), successAction2.getId());
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {successAction1, failAction, successAction2});
        success = SafeModeController.getInstance().executeActions(oneFailure);
        Assert.assertEquals(
                "Overall status should be failure if at least one action fails",
                success,
                SafeModeController.SafeModeExecutionResult.ACTION_FAILED);
        histogramExpectation.assertExpected(
                "Overall status should be failure if at least one action fails");
        Assert.assertEquals(
                "successAction1 should have been executed again", 2, successAction1.getCallCount());
        Assert.assertEquals(
                "failAction should have been executed exactly 1 time",
                1,
                failAction.getCallCount());
        Assert.assertEquals(
                "successAction2 should have been executed again", 2, successAction2.getCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeAction_namesRecordedAsExpected() throws Throwable {
        HistogramWatcher.Builder histogramWatcherBuilder = HistogramWatcher.newBuilder();
        histogramWatcherBuilder.expectIntRecords(
                "Android.WebView.SafeMode.ActionName",
                SafeModeController.sSafeModeActionLoggingMap.get(
                        SafeModeActionIds.DELETE_VARIATIONS_SEED),
                SafeModeController.sSafeModeActionLoggingMap.get(
                        SafeModeActionIds.FAST_VARIATIONS_SEED));
        HistogramWatcher watcher = histogramWatcherBuilder.build();

        TestSafeModeAction successAction1 =
                new TestSafeModeAction(SafeModeActionIds.DELETE_VARIATIONS_SEED);
        TestSafeModeAction successAction2 =
                new TestSafeModeAction(SafeModeActionIds.FAST_VARIATIONS_SEED);
        Set<String> allSuccessful = asSet(successAction1.getId(), successAction2.getId());
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {successAction1, successAction2});
        SafeModeController.getInstance().executeActions(allSuccessful);
        watcher.assertExpected(
                "SafeModeAction names should be recorded: ["
                        + successAction1.getId()
                        + ", "
                        + successAction2.getId()
                        + "]");
        Assert.assertEquals(
                "successAction1 should have been executed exactly 1 time",
                1,
                successAction1.getCallCount());
        Assert.assertEquals(
                "successAction2 should have been executed exactly 1 time",
                1,
                successAction2.getCallCount());
        SafeModeController.getInstance().unregisterActionsForTesting();

        histogramWatcherBuilder = HistogramWatcher.newBuilder();
        histogramWatcherBuilder.expectIntRecords(
                "Android.WebView.SafeMode.ActionName",
                SafeModeController.sSafeModeActionLoggingMap.get(SafeModeActionIds.NOOP),
                SafeModeController.sSafeModeActionLoggingMap.get(
                        SafeModeActionIds.DISABLE_ANDROID_AUTOFILL));
        watcher = histogramWatcherBuilder.build();
        successAction1 = new TestSafeModeAction(SafeModeActionIds.NOOP);
        successAction2 = new TestSafeModeAction(SafeModeActionIds.DISABLE_ANDROID_AUTOFILL);
        allSuccessful = asSet(successAction1.getId(), successAction2.getId());
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {successAction1, successAction2});
        SafeModeController.getInstance().executeActions(allSuccessful);
        watcher.assertExpected(
                "SafeModeAction names should be recorded: ["
                        + successAction1.getId()
                        + ", "
                        + successAction2.getId()
                        + "]");
        Assert.assertEquals(
                "successAction1 should have been executed exactly 1 time",
                1,
                successAction1.getCallCount());
        Assert.assertEquals(
                "successAction2 should have been executed exactly 1 time",
                1,
                successAction2.getCallCount());
        SafeModeController.getInstance().unregisterActionsForTesting();

        histogramWatcherBuilder = HistogramWatcher.newBuilder();
        histogramWatcherBuilder.expectIntRecords(
                "Android.WebView.SafeMode.ActionName",
                SafeModeController.sSafeModeActionLoggingMap.get(
                        SafeModeActionIds.DISABLE_AW_SAFE_BROWSING),
                SafeModeController.sSafeModeActionLoggingMap.get(
                        SafeModeActionIds.DISABLE_ORIGIN_TRIALS));
        watcher = histogramWatcherBuilder.build();
        successAction1 = new TestSafeModeAction(SafeModeActionIds.DISABLE_AW_SAFE_BROWSING);
        successAction2 = new TestSafeModeAction(SafeModeActionIds.DISABLE_ORIGIN_TRIALS);
        allSuccessful = asSet(successAction1.getId(), successAction2.getId());
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {successAction1, successAction2});
        SafeModeController.getInstance().executeActions(allSuccessful);
        watcher.assertExpected(
                "SafeModeAction names should be recorded: ["
                        + successAction1.getId()
                        + ", "
                        + successAction2.getId()
                        + "]");
        Assert.assertEquals(
                "successAction1 should have been executed exactly 1 time",
                1,
                successAction1.getCallCount());
        Assert.assertEquals(
                "successAction2 should have been executed exactly 1 time",
                1,
                successAction2.getCallCount());
    }

    @Test
    @MediumTest
    public void testSafeModeAction_canRegisterBrowserActions() throws Exception {
        // Validity check: verify we can register the production SafeModeAction list. As long as
        // this finishes without throwing, assume the list is in good shape (e.g., no duplicate
        // SafeModeAction IDs).
        SafeModeController.getInstance().registerActions(BrowserSafeModeActionList.sList);
    }

    @Test
    @MediumTest
    public void testFastVariations_executesSuccessWithLocalSeed() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            VariationsTestUtils.writeMockSeed(newFile);
            // Create time stamp file so the mitigation will use the local app directory instead
            // of waiting for the ContentProvider to provide a seed for it.
            VariationsUtils.updateStampTime();
            FastVariationsSeedSafeModeAction action =
                    new FastVariationsSeedSafeModeAction(TEST_WEBVIEW_PACKAGE_NAME);
            boolean success = action.execute();
            Assert.assertTrue("VariationsSeedSafeModeAction should indicate success", success);

            TestLoader loader = new TestLoader(new TestLoaderResult());
            loader.startVariationsInit();
            boolean loadedSeed = loader.finishVariationsInit();
            Assert.assertTrue(
                    "Did not load a variations seed even though it should have been available",
                    loadedSeed);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    @Test
    @MediumTest
    public void testFastVariations_executesFailureWithLocalSeedExpiredAndNoSeedFromContentProvider()
            throws Exception {
        long startingTime = 54000L;
        AwVariationsSeedFetcher.setMocks(mScheduler, mDownloader);
        AwVariationsSeedFetcher.setUseSmallJitterForTesting();
        FastVariationsSeedSafeModeAction action =
                new FastVariationsSeedSafeModeAction(TEST_WEBVIEW_PACKAGE_NAME);

        // Mimic embedded data directory that lives separately from nonembedded data directory for
        // testing
        File seedFileDirectory =
                new File(
                        PathUtils.getDataDirectory() + File.separator,
                        "embedded-data-directory-for-test");
        Assert.assertTrue("Seed file directory already exists", seedFileDirectory.mkdir());
        File embeddedSeedFile =
                new File(seedFileDirectory.getPath() + File.separator, "variations_seed");
        Assert.assertTrue("Seed file already exists", embeddedSeedFile.createNewFile());
        // mark the embedded seed file as expired
        embeddedSeedFile.setLastModified(
                startingTime + VariationsFastFetchModeUtils.MAX_ALLOWABLE_SEED_AGE_MS + 1L);
        FastVariationsSeedSafeModeAction.setAlternateSeedFilePath(embeddedSeedFile);

        try {
            boolean success = action.execute();
            Assert.assertFalse(
                    "FastVariationsSeedSafeModeAction should indicate"
                            + " failure with no variations seed",
                    success);
        } finally {
            VariationsTestUtils.deleteSeeds();
            FileUtils.recursivelyDeleteFile(seedFileDirectory, FileUtils.DELETE_ALL);
        }
    }

    @Test
    @MediumTest
    public void testFastVariations_executesSuccessWithLocalSeedExpiredAndSeedFromContentProvider()
            throws Exception {
        long startingTime = 54000L;
        AwVariationsSeedFetcher.setMocks(mScheduler, mDownloader);
        AwVariationsSeedFetcher.setUseSmallJitterForTesting();
        FastVariationsSeedSafeModeAction action =
                new FastVariationsSeedSafeModeAction(TEST_WEBVIEW_PACKAGE_NAME);

        File nonEmbeddedSeedFile = VariationsUtils.getSeedFile();
        Assert.assertTrue("Seed file already exists", nonEmbeddedSeedFile.createNewFile());
        VariationsTestUtils.writeMockSeed(nonEmbeddedSeedFile);
        // Mark the nonembedded seed file as unexpired
        final Date date = mock(Date.class);
        when(date.getTime()).thenReturn(startingTime + 1L);
        AwVariationsSeedFetcher.setDateForTesting(date);
        nonEmbeddedSeedFile.setLastModified(startingTime);

        // Mimic embedded data directory that lives separately from nonembedded data directory for
        // testing
        File seedFileDirectory =
                new File(
                        PathUtils.getDataDirectory() + File.separator,
                        "embedded-data-directory-for-test");
        Assert.assertTrue("Seed file directory already exists", seedFileDirectory.mkdir());
        File embeddedSeedFile =
                new File(seedFileDirectory.getPath() + File.separator, "variations_seed");
        Assert.assertTrue("Seed file already exists", embeddedSeedFile.createNewFile());
        // mark the embedded seed file as expired
        embeddedSeedFile.setLastModified(
                startingTime + VariationsFastFetchModeUtils.MAX_ALLOWABLE_SEED_AGE_MS + 1L);
        FastVariationsSeedSafeModeAction.setAlternateSeedFilePath(embeddedSeedFile);

        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            VariationsTestUtils.writeMockSeed(oldFile);
            VariationsTestUtils.writeMockSeed(newFile);
            setSafeMode(Arrays.asList(action.getId()));

            boolean success = action.execute();
            Assert.assertTrue(
                    "FastVariationsSeedSafeModeAction should not indicate"
                            + " failure with variations seed in ContentProvider's data directory",
                    success);
        } finally {
            VariationsTestUtils.deleteSeeds();
            FileUtils.recursivelyDeleteFile(seedFileDirectory, FileUtils.DELETE_ALL);
        }
    }

    @Test
    @MediumTest
    public void testFastVariations_executesSuccessWithLocalSeedAlmostExpired() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            VariationsTestUtils.writeMockSeed(newFile);
            // Create an almost expired time stamp file so the mitigation will not request a new
            // seed from the ContentProvider
            long now = new Date().getTime();
            VariationsUtils.updateStampTime(
                    now + VariationsFastFetchModeUtils.MAX_ALLOWABLE_SEED_AGE_MS - 1);
            FastVariationsSeedSafeModeAction action =
                    new FastVariationsSeedSafeModeAction(TEST_WEBVIEW_PACKAGE_NAME);
            boolean success = action.execute();
            Assert.assertTrue("VariationsSeedSafeModeAction should indicate success", success);

            TestLoader loader = new TestLoader(new TestLoaderResult());
            loader.startVariationsInit();
            boolean loadedSeed = loader.finishVariationsInit();
            Assert.assertTrue(
                    "Did not load a variations seed even though it should have been available",
                    loadedSeed);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    @Test
    @MediumTest
    public void testFastVariations_failsWithoutVariationsSeed() throws Exception {
        VariationsTestUtils.deleteSeeds(); // ensure no seed files exist
        FastVariationsSeedSafeModeAction action =
                new FastVariationsSeedSafeModeAction(TEST_WEBVIEW_PACKAGE_NAME);
        // Since no seed file exists in the embedding app directory, and the ContentProvider
        // does not have a valid seed to return to the FastVariationsSeedSafeModeAction,
        // it fails with no valid seed to load.
        boolean success = action.execute();
        Assert.assertFalse(
                "FastVariationsSeedSafeModeAction should indicate"
                        + " failure with no variations seed",
                success);
        TestLoader loader = new TestLoader(new TestLoaderResult());
        loader.startVariationsInit();
        boolean loadedSeed = loader.finishVariationsInit();
        Assert.assertFalse(
                "Loaded a variations seed even though it should not have one to load.", loadedSeed);
    }

    @Test
    @MediumTest
    public void testVariations_deletesSeedFiles() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            VariationsTestUtils.writeMockSeed(newFile);
            VariationsSeedSafeModeAction action = new VariationsSeedSafeModeAction();
            boolean success = action.execute();
            Assert.assertTrue("VariationsSeedSafeModeAction should indicate success", success);
            Assert.assertFalse(
                    "Old seed should have been deleted but it still exists", oldFile.exists());
            Assert.assertFalse(
                    "New seed should have been deleted but it still exists", newFile.exists());
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    @Test
    @MediumTest
    public void testVariations_doesNotLoadExperiments() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            Assert.assertTrue("Seed file already exists", oldFile.createNewFile());
            Assert.assertTrue("New seed file already exists", newFile.createNewFile());
            VariationsTestUtils.writeMockSeed(oldFile);
            VariationsTestUtils.writeMockSeed(newFile);
            VariationsSeedSafeModeAction action = new VariationsSeedSafeModeAction();
            boolean success = action.execute();
            Assert.assertTrue("VariationsSeedSafeModeAction should indicate success", success);

            TestLoader loader = new TestLoader(new TestLoaderResult());
            loader.startVariationsInit();
            boolean loadedSeed = loader.finishVariationsInit();
            Assert.assertFalse(
                    "Loaded a variations seed even though it should have been deleted by SafeMode",
                    loadedSeed);
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    @Test
    @MediumTest
    public void testVariations_doesNothingIfSeedDoesNotExist() throws Exception {
        try {
            File oldFile = VariationsUtils.getSeedFile();
            File newFile = VariationsUtils.getNewSeedFile();
            VariationsSeedSafeModeAction action = new VariationsSeedSafeModeAction();
            boolean success = action.execute();
            Assert.assertTrue("VariationsSeedSafeModeAction should indicate success", success);
            Assert.assertFalse("Old seed should never have existed", oldFile.exists());
            Assert.assertFalse("New seed should never have existed", newFile.exists());
        } finally {
            VariationsTestUtils.deleteSeeds();
        }
    }

    @Test
    @MediumTest
    public void testTrustedPackage_invalidCert() throws Exception {
        TrustedPackage invalidPackage =
                new TrustedPackage(TEST_WEBVIEW_PACKAGE_NAME, FAKE_CERT_HASH, null);
        Assert.assertFalse(
                "wrong certificate should not verify",
                invalidPackage.verify(TEST_WEBVIEW_PACKAGE_NAME));
    }

    private static class TestTrustedPackage extends TrustedPackage {
        private boolean mIsDebug = true;

        TestTrustedPackage(String packageName, byte[] release, byte[] debug) {
            super(packageName, release, debug);
        }

        @Override
        protected boolean isDebugAndroid() {
            return mIsDebug;
        }

        void setDebugBuildForTesting(boolean debug) {
            mIsDebug = debug;
        }
    }

    @Test
    @MediumTest
    public void testTrustedPackage_wrongPackageName() throws Exception {
        TrustedPackage webviewTestShell =
                new TestTrustedPackage(TEST_WEBVIEW_PACKAGE_NAME, TEST_WEBVIEW_CERT_HASH, null);
        Assert.assertFalse(
                "Wrong pacakge name should not verify",
                webviewTestShell.verify("com.fake.package.name"));
    }

    @Test
    @MediumTest
    public void testTrustedPackage_eitherCertCanMatchOnDebugAndroid() throws Exception {
        TrustedPackage webviewTestShell =
                new TrustedPackage(TEST_WEBVIEW_PACKAGE_NAME, TEST_WEBVIEW_CERT_HASH, null);
        Assert.assertTrue(
                "The WebView test shell should match itself",
                webviewTestShell.verify(TEST_WEBVIEW_PACKAGE_NAME));
        // Adding a non-matching certificate should not change anything (we should still trust
        // this).
        webviewTestShell =
                new TestTrustedPackage(
                        TEST_WEBVIEW_PACKAGE_NAME, TEST_WEBVIEW_CERT_HASH, FAKE_CERT_HASH);
        Assert.assertTrue(
                "The WebView test shell should match itself",
                webviewTestShell.verify(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    public void testTrustedPackage_debugCertsOnlyTrustedOnDebugAndroid() throws Exception {
        // Use a fake release cert and real debug cert so this package can only be trusted on a
        // debug build.
        TestTrustedPackage webviewTestShell =
                new TestTrustedPackage(
                        TEST_WEBVIEW_PACKAGE_NAME, FAKE_CERT_HASH, TEST_WEBVIEW_CERT_HASH);

        webviewTestShell.setDebugBuildForTesting(true);
        Assert.assertTrue(
                "Debug cert should be trusted on debug Android build",
                webviewTestShell.verify(TEST_WEBVIEW_PACKAGE_NAME));

        webviewTestShell.setDebugBuildForTesting(false);
        Assert.assertFalse(
                "Debug cert should not be trusted on release Android build",
                webviewTestShell.verify(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @MediumTest
    public void testTrustedPackage_verifyWebViewTestShell() throws Exception {
        TrustedPackage webviewTestShell =
                new TestTrustedPackage(TEST_WEBVIEW_PACKAGE_NAME, TEST_WEBVIEW_CERT_HASH, null);
        Assert.assertTrue(
                "The WebView test shell should match itself",
                webviewTestShell.verify(TEST_WEBVIEW_PACKAGE_NAME));
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_executeNonEmbeddedActionWhenRegisteredAndEnabled()
            throws Throwable {
        TestNonEmbeddedSafeModeAction testAction = new TestNonEmbeddedSafeModeAction("test");

        // Register test action
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});
        // Enable test action
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should be executed once", 1, testAction.getActivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void
            testNonEmbeddedSafeModeActionList_executeNonEmbeddedActionWhenRegisteredAndDisabled()
                    throws Throwable {
        TestNonEmbeddedSafeModeAction testAction = new TestNonEmbeddedSafeModeAction("test");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});

        // Enable test action
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should be executed once when enabled",
                1,
                testAction.getActivatedCallCount());

        // Disable test action
        setSafeMode(Arrays.asList());
        Assert.assertEquals(
                "Test action should be executed when previously "
                        + "enabled and current command is to disable it",
                1,
                testAction.getDeactivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_doNotExecuteNonEmbeddedActionAlreadyEnabled()
            throws Throwable {
        TestNonEmbeddedSafeModeAction testAction = new TestNonEmbeddedSafeModeAction("test");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});

        // Enable test action
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should be executed once when enabled",
                1,
                testAction.getActivatedCallCount());

        // Enable test action again Since test action was previously enabled and it is being
        // enabled again, it will not execute again since there is no state change
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should not be executed when previously "
                        + "enabled and current command is to enable it",
                1,
                testAction.getActivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_doNotExecuteNonEmbeddedActionAlreadyDisabled()
            throws Throwable {
        TestNonEmbeddedSafeModeAction testAction = new TestNonEmbeddedSafeModeAction("test");
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});

        // Enable test action
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should be executed once when enabled",
                1,
                testAction.getActivatedCallCount());

        // Disable test action
        setSafeMode(Arrays.asList());
        Assert.assertEquals(
                "Test action should be executed when previously "
                        + "enabled and current command is to disable it",
                1,
                testAction.getDeactivatedCallCount());

        // Disable test action again
        setSafeMode(Arrays.asList());
        Assert.assertEquals(
                "Test action should not be executed when previously"
                        + " disabled and current command is to disable it",
                1,
                testAction.getDeactivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_doNotExecuteNonEmbeddedActionWhenUnregistered()
            throws Throwable {
        TestNonEmbeddedSafeModeAction testAction = new TestNonEmbeddedSafeModeAction("test");
        // Register empty action list
        SafeModeController.getInstance().registerActions(new SafeModeAction[] {});

        // Enable test action
        setSafeMode(Arrays.asList(testAction.getId()));
        Assert.assertEquals(
                "Test action should not be executed when enabled and not registered",
                0,
                testAction.getActivatedCallCount());

        // Disable test action
        setSafeMode(Arrays.asList());
        Assert.assertEquals(
                "Test action should not be executed when disabled and not registered",
                0,
                testAction.getDeactivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_nonEmbeddedActionsFailureIsCaptured()
            throws Throwable {
        TestNonEmbeddedSafeModeAction failingTestAction =
                new TestNonEmbeddedSafeModeAction("test", false);
        TestNonEmbeddedSafeModeAction passingTestAction =
                new TestNonEmbeddedSafeModeAction("passingtest");
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {failingTestAction, passingTestAction});

        Assert.assertFalse(
                "Overall status is failure if mitigations indicate"
                        + "failure when SafeMode is activated",
                NonEmbeddedSafeModeActionsSetupCleanup.executeNonEmbeddedActionsOnStateChange(
                        new HashSet<>(Arrays.asList()),
                        new HashSet<>(Arrays.asList(failingTestAction.getId()))));
        Assert.assertFalse(
                "Overall status is failure if mitigations indicate"
                        + "failure when SafeMode is deactivated",
                NonEmbeddedSafeModeActionsSetupCleanup.executeNonEmbeddedActionsOnStateChange(
                        new HashSet<>(Arrays.asList(failingTestAction.getId())),
                        new HashSet<>(Arrays.asList())));
        Assert.assertFalse(
                "Overall status is failure if at least one mitigation indicates "
                        + "failure when SafeMode is activated",
                NonEmbeddedSafeModeActionsSetupCleanup.executeNonEmbeddedActionsOnStateChange(
                        new HashSet<>(Arrays.asList()),
                        new HashSet<>(
                                Arrays.asList(
                                        failingTestAction.getId(), passingTestAction.getId()))));

        setSafeMode(Arrays.asList(passingTestAction.getId()));
        Assert.assertTrue(
                "Overall status is success if all mitigations success",
                NonEmbeddedSafeModeActionsSetupCleanup.executeNonEmbeddedActionsOnStateChange(
                        new HashSet<>(Arrays.asList()),
                        new HashSet<>(Arrays.asList(passingTestAction.getId()))));
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void
            testNonEmbeddedSafeModeActionList_multipleNonEmbeddedActionsNotExecutedWhenNotRegistered()
                    throws Throwable {
        TestNonEmbeddedSafeModeAction testAction1 = new TestNonEmbeddedSafeModeAction("test1");
        TestNonEmbeddedSafeModeAction testAction2 = new TestNonEmbeddedSafeModeAction("test2");
        TestNonEmbeddedSafeModeAction testAction3 =
                new TestNonEmbeddedSafeModeAction("test3", false);

        // Actions are not yet registered, activating them
        setSafeMode(Arrays.asList(testAction1.getId(), testAction2.getId(), testAction3.getId()));
        Assert.assertEquals(
                "Test action 1 should not be executed", 0, testAction1.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should not be executed", 0, testAction2.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should not be executed", 0, testAction3.getActivatedCallCount());

        // Register all three actions
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction1, testAction2, testAction3});

        // turn off
        setSafeMode(Arrays.asList());
        Assert.assertEquals(
                "Test action 1 should be executed once", 1, testAction1.getDeactivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should be executed once", 1, testAction2.getDeactivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should be executed once", 1, testAction3.getDeactivatedCallCount());

        // Unregister test action 2
        SafeModeController.getInstance().unregisterActionsForTesting();
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction1, testAction3});

        // turn on
        setSafeMode(Arrays.asList(testAction1.getId(), testAction2.getId(), testAction3.getId()));
        Assert.assertEquals(
                "Test action 1 should be activated once", 1, testAction1.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should not be activated since it's not in the list",
                0,
                testAction2.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should be activated once", 1, testAction3.getActivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testNonEmbeddedSafeModeActionList_turnOffMultipleNonEmbeddedActions()
            throws Throwable {
        TestNonEmbeddedSafeModeAction testAction1 = new TestNonEmbeddedSafeModeAction("test1");
        TestNonEmbeddedSafeModeAction testAction2 = new TestNonEmbeddedSafeModeAction("test2");
        TestNonEmbeddedSafeModeAction testAction3 =
                new TestNonEmbeddedSafeModeAction("test3", false);
        SafeModeController.getInstance()
                .registerActions(new SafeModeAction[] {testAction1, testAction2, testAction3});

        Assert.assertEquals(
                "Test action 1 should not be executed", 0, testAction1.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should not be executed", 0, testAction2.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should not be executed", 0, testAction3.getActivatedCallCount());
        setSafeMode(Arrays.asList(testAction1.getId(), testAction2.getId(), testAction3.getId()));

        setSafeMode(Arrays.asList(testAction2.getId()));
        Assert.assertEquals(
                "Test action 1 should be activated once", 1, testAction1.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 1 should be deactivated once",
                1,
                testAction1.getDeactivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should be activated once", 1, testAction2.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 2 should not be deactivated",
                0,
                testAction2.getDeactivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should be activated once", 1, testAction3.getActivatedCallCount());
        Assert.assertEquals(
                "Test action 3 should be deactivated once",
                1,
                testAction3.getDeactivatedCallCount());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testSafeModeActionList_turnOffSafeModeSeedFetch() throws Throwable {
        AwVariationsSeedFetcher.setUseSmallJitterForTesting();
        NonEmbeddedFastVariationsSeedSafeModeAction testAction =
                new NonEmbeddedFastVariationsSeedSafeModeAction();
        AwVariationsSeedFetcher.setMocks(mScheduler, mDownloader);

        SafeModeController.getInstance().registerActions(new SafeModeAction[] {testAction});
        SafeModeAction[] actions = SafeModeController.getInstance().getRegisteredActions();
        Assert.assertThat("Actions list should not be empty", actions.length, greaterThan(0));

        mScheduler.assertNotScheduled();
        setSafeMode(Arrays.asList(testAction.getId()));
        mScheduler.waitForMessageCallback();
        mScheduler.assertScheduled(JOB_ID);

        setSafeMode(Arrays.asList());
        mScheduler.assertNotScheduled();
    }

    private void setSafeMode(List<String> actions) throws RemoteException {
        Intent intent = new Intent(ContextUtils.getApplicationContext(), SafeModeService.class);
        try (ServiceConnectionHelper helper =
                new ServiceConnectionHelper(intent, Context.BIND_AUTO_CREATE)) {
            ISafeModeService service = ISafeModeService.Stub.asInterface(helper.getBinder());
            service.setSafeMode(actions);
        }
    }
}