/*
 * Copyright (C) 2018 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.server.adb;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.content.Context;
import android.provider.Settings;
import android.util.Log;

import androidx.test.InstrumentationRegistry;

import com.android.server.FgThread;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(JUnit4.class)
public final class AdbDebuggingManagerTest {

    private static final String TAG = "AdbDebuggingManagerTest";

    // This component is passed to the AdbDebuggingManager to act as the activity that can confirm
    // unknown adb keys.  An overlay package was first attempted to override the
    // config_customAdbPublicKeyConfirmationComponent config, but the value from that package was
    // not being read.
    private static final String ADB_CONFIRM_COMPONENT =
            "com.android.frameworks.servicestests/"
                    + "com.android.server.adb.AdbDebuggingManagerTestActivity";

    // The base64 encoding of the values 'test key 1' and 'test key 2'.
    private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo= test@android.com";
    private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo= test@android.com";

    // This response is received from the AdbDebuggingHandler when the key is allowed to connect
    private static final String RESPONSE_KEY_ALLOWED = "OK";
    // This response is received from the AdbDebuggingHandler when the key is not allowed to connect
    private static final String RESPONSE_KEY_DENIED = "NO";

    // wait up to 5 seconds for any blocking queries
    private static final long TIMEOUT = 5000;
    private static final TimeUnit TIMEOUT_TIME_UNIT = TimeUnit.MILLISECONDS;

    private Context mContext;
    private AdbDebuggingManager mManager;
    private AdbDebuggingManager.AdbDebuggingThread mThread;
    private AdbDebuggingManager.AdbDebuggingHandler mHandler;
    private AdbDebuggingManager.AdbKeyStore mKeyStore;
    private BlockingQueue<TestResult> mBlockingQueue;
    private long mOriginalAllowedConnectionTime;
    private File mAdbKeyXmlFile;
    private File mAdbKeyFile;

    @Before
    public void setUp() throws Exception {
        mContext = InstrumentationRegistry.getContext();
        mAdbKeyFile = new File(mContext.getFilesDir(), "adb_keys");
        if (mAdbKeyFile.exists()) {
            mAdbKeyFile.delete();
        }
        mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile);
        mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
        if (mAdbKeyXmlFile.exists()) {
            mAdbKeyXmlFile.delete();
        }
        mThread = new AdbDebuggingThreadTest();
        mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
        mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
        mBlockingQueue = new ArrayBlockingQueue<>(1);
    }

    @After
    public void tearDown() throws Exception {
        mKeyStore.deleteKeyStore();
        setAllowedConnectionTime(mOriginalAllowedConnectionTime);
    }

    /**
     * Sets the allowed connection time within which a subsequent connection from a key for which
     * the user selected the 'Always allow' option will be allowed without user interaction.
     */
    private void setAllowedConnectionTime(long connectionTime) {
        Settings.Global.putLong(mContext.getContentResolver(),
                Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime);
    };

    @Test
    public void testAllowNewKeyOnce() throws Exception {
        // Verifies the behavior when a new key first attempts to connect to a device. During the
        // first connection the ADB confirmation activity should be launched to prompt the user to
        // allow the connection with an option to always allow connections from this key.

        // Verify if the user allows the key but does not select the option to 'always
        // allow' that the connection is allowed but the key is not stored.
        runAdbTest(TEST_KEY_1, true, false, false);

        // Persist the keystore to ensure that the key is not written to the adb_keys file.
        persistKeyStore();
        assertFalse(
                "A key for which the 'always allow' option is not selected must not be written "
                        + "to the adb_keys file",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testDenyNewKey() throws Exception {
        // Verifies if the user does not allow the key then the connection is not allowed and the
        // key is not stored.
        runAdbTest(TEST_KEY_1, false, false, false);
    }

    @Test
    public void testDisconnectAlwaysAllowKey() throws Exception {
        // When a key is disconnected from a device ADB should send a disconnect message; this
        // message should trigger an update of the last connection time for the currently connected
        // key.

        // Allow a connection from a new key with the 'Always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);

        // Get the last connection time for the currently connected key to verify that it is updated
        // after the disconnect.
        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);

        // Sleep for a small amount of time to ensure a difference can be observed in the last
        // connection time after a disconnect.
        Thread.sleep(10);

        // Send the disconnect message for the currently connected key to trigger an update of the
        // last connection time.
        disconnectKey(TEST_KEY_1);
        assertNotEquals(
                "The last connection time was not updated after the disconnect",
                lastConnectionTime,
                mKeyStore.getLastConnectionTime(TEST_KEY_1));
    }

    @Test
    public void testDisconnectAllowedOnceKey() throws Exception {
        // When a key is disconnected ADB should send a disconnect message; this message should
        // essentially result in a noop for keys that the user only allows once since the last
        // connection time is not maintained for these keys.

        // Allow a connection from a new key with the 'Always allow' option set to false
        runAdbTest(TEST_KEY_1, true, false, false);

        // Send the disconnect message for the currently connected key.
        disconnectKey(TEST_KEY_1);

        // Verify that the disconnected key is not automatically allowed on a subsequent connection.
        runAdbTest(TEST_KEY_1, true, false, false);
    }

    @Test
    public void testAlwaysAllowConnectionFromKey() throws Exception {
        // Verifies when the user selects the 'Always allow' option for the current key that
        // subsequent connection attempts from that key are allowed.

        // Allow a connection from a new key with the 'Always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);

        // Send a persist keystore message to force the key to be written to the adb_keys file
        persistKeyStore();

        // Verify the key is in the adb_keys file to ensure subsequent connections are allowed by
        // adbd.
        assertTrue("The key was not in the adb_keys file after persisting the keystore",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testOriginalAlwaysAllowBehavior() throws Exception {
        // If the Settings.Global.ADB_ALLOWED_CONNECTION_TIME setting is set to 0 then the original
        // behavior of 'Always allow' should be restored.

        // Accept the test key with the 'Always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);

        // Set the connection time to 0 to restore the original behavior.
        setAllowedConnectionTime(0);

        // Set the last connection time to the test key to a very small value to ensure it would
        // fail the new test but would be allowed with the original behavior.
        mKeyStore.setLastConnectionTime(TEST_KEY_1, 1);

        // Verify that the key is in the adb_keys file to ensure subsequent connections are
        // automatically allowed by adbd.
        persistKeyStore();
        assertTrue("The key was not in the adb_keys file after persisting the keystore",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testLastConnectionTimeUpdatedByScheduledJob() throws Exception {
        // If a development device is left connected to a system beyond the allowed connection time
        // a reboot of the device while connected could make it appear as though the last connection
        // time is beyond the allowed window. A scheduled job runs daily while a key is connected
        // to update the last connection time to the current time; this ensures if the device is
        // rebooted while connected to a system the last connection time should be within 24 hours.

        // Allow the key to connect with the 'Always allow' option selected
        runAdbTest(TEST_KEY_1, true, true, false);

        // Get the current last connection time for comparison after the scheduled job is run
        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);

        // Sleep a small amount of time to ensure that the updated connection time changes
        Thread.sleep(10);

        // Send a message to the handler to update the last connection time for the active key
        updateKeyStore();
        assertNotEquals(
                "The last connection time of the key was not updated after the update key "
                        + "connection time message",
                lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
    }

    @Test
    public void testKeystorePersisted() throws Exception {
        // After any updates are made to the key store a message should be sent to persist the
        // key store. This test verifies that a key that is always allowed is persisted in the key
        // store along with its last connection time.

        // Allow the key to connect with the 'Always allow' option selected
        runAdbTest(TEST_KEY_1, true, true, false);

        // Send a message to the handler to persist the updated keystore and verify a new key store
        // backed by the XML file contains the key.
        persistKeyStore();
        assertTrue(
                "The key with the 'Always allow' option selected was not persisted in the keystore",
                mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1));

        // Get the current last connection time to ensure it is updated in the persisted keystore.
        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);

        // Sleep a small amount of time to ensure the last connection time is updated.
        Thread.sleep(10);

        // Send a message to the handler to update the last connection time for the active key.
        updateKeyStore();

        // Persist the updated last connection time and verify a new key store backed by the XML
        // file contains the updated connection time.
        persistKeyStore();
        assertNotEquals(
                "The last connection time in the key file was not updated after the update "
                        + "connection time message", lastConnectionTime,
                mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1));
        // Verify that the key is in the adb_keys file
        assertTrue("The key was not in the adb_keys file after persisting the keystore",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testAdbClearRemovesActiveKey() throws Exception {
        // If the user selects the option to 'Revoke USB debugging authorizations' while an 'Always
        // allow' key is connected that key should be deleted as well.

        // Allow the key to connect with the 'Always allow' option selected
        runAdbTest(TEST_KEY_1, true, true, false);

        // Send a message to the handler to clear the adb authorizations.
        clearKeyStore();

        // Send a message to disconnect the currently connected key
        disconnectKey(TEST_KEY_1);
        assertFalse(
                "The currently connected 'always allow' key must not be authorized after an adb"
                        + " clear message.",
                mKeyStore.isKeyAuthorized(TEST_KEY_1));

        // The key should not be in the adb_keys file after clearing the authorizations.
        assertFalse("The key must not be in the adb_keys file after clearing authorizations",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testAdbGrantRevokedIfLastConnectionBeyondAllowedTime() throws Exception {
        // If the user selects the 'Always allow' option then subsequent connections from the key
        // will be allowed as long as the connection is within the allowed window. Once the last
        // connection time is beyond this window the user should be prompted to allow the key again.

        // Allow the key to connect with the 'Always allow' option selected
        runAdbTest(TEST_KEY_1, true, true, false);

        // Set the allowed window to a small value to ensure the time is beyond the allowed window.
        setAllowedConnectionTime(1);

        // Sleep for a small amount of time to exceed the allowed window.
        Thread.sleep(10);

        // The AdbKeyStore has a method to get the time of the next key expiration to ensure the
        // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs
        // first.
        assertEquals("The time of the next key expiration must be 0.", 0,
                mKeyStore.getNextExpirationTime());

        // Persist the key store and verify that the key is no longer in the adb_keys file.
        persistKeyStore();
        assertFalse(
                "The key must not be in the adb_keys file after the allowed time has elapsed.",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testLastConnectionTimeCannotBeSetBack() throws Exception {
        // When a device is first booted there is a possibility that the system time will be set to
        // the build time of the system image. If a device is connected to a system during a reboot
        // this could cause the connection time to be set in the past; if the device time is not
        // corrected before the device is disconnected then a subsequent connection with the time
        // corrected would appear as though the last connection time was beyond the allowed window,
        // and the user would be required to authorize the connection again. This test verifies that
        // the AdbKeyStore does not update the last connection time if it is less than the
        // previously written connection time.

        // Allow the key to connect with the 'Always allow' option selected
        runAdbTest(TEST_KEY_1, true, true, false);

        // Get the last connection time that was written to the key store.
        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);

        // Attempt to set the last connection time to 1970
        mKeyStore.setLastConnectionTime(TEST_KEY_1, 0);
        assertEquals(
                "The last connection time in the adb key store must not be set to a value less "
                        + "than the previous connection time",
                lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));

        // Attempt to set the last connection time just beyond the allowed window.
        mKeyStore.setLastConnectionTime(TEST_KEY_1,
                Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1)));
        assertEquals(
                "The last connection time in the adb key store must not be set to a value less "
                        + "than the previous connection time",
                lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
    }

    @Test
    public void testAdbKeyRemovedByScheduledJob() throws Exception {
        // When a key is automatically allowed it should be stored in the adb_keys file. A job is
        // then scheduled daily to update the connection time of the currently connected key, and if
        // no connected key exists the key store is updated to purge expired keys. This test
        // verifies that after a key's expiration time has been reached that it is no longer
        // in the key store nor the adb_keys file

        // Set the allowed time to the default to ensure that any modification to this value do not
        // impact this test.
        setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);

        // Allow both test keys to connect with the 'always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);
        runAdbTest(TEST_KEY_2, true, true, false);
        disconnectKey(TEST_KEY_1);
        disconnectKey(TEST_KEY_2);

        // Persist the key store and verify that both keys are in the key store and adb_keys file.
        persistKeyStore();
        assertTrue(
                "Test key 1 must be in the adb_keys file after selecting the 'always allow' "
                        + "option",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
        assertTrue(
                "Test key 1 must be in the adb key store after selecting the 'always allow' "
                        + "option",
                mKeyStore.isKeyAuthorized(TEST_KEY_1));
        assertTrue(
                "Test key 2 must be in the adb_keys file after selecting the 'always allow' "
                        + "option",
                isKeyInFile(TEST_KEY_2, mAdbKeyFile));
        assertTrue(
                "Test key 2 must be in the adb key store after selecting the 'always allow' option",
                mKeyStore.isKeyAuthorized(TEST_KEY_2));

        // Set test key 1's last connection time to a small value and persist the keystore to ensure
        // it is cleared out after the next key store update.
        mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
        updateKeyStore();
        assertFalse(
                "Test key 1 must no longer be in the adb_keys file after its timeout period is "
                        + "reached",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
        assertFalse(
                "Test key 1 must no longer be in the adb key store after its timeout period is "
                        + "reached",
                mKeyStore.isKeyAuthorized(TEST_KEY_1));
        assertTrue(
                "Test key 2 must still be in the adb_keys file after test key 1's timeout "
                        + "period is reached",
                isKeyInFile(TEST_KEY_2, mAdbKeyFile));
        assertTrue(
                "Test key 2 must still be in the adb key store after test key 1's timeout period "
                        + "is reached",
                mKeyStore.isKeyAuthorized(TEST_KEY_2));
    }

    @Test
    public void testKeystoreExpirationTimes() throws Exception {
        // When one or more keys are always allowed a daily job is scheduled to update the
        // connection time of the connected key and to purge any expired keys. The keystore provides
        // a method to obtain the expiration time of the next key to expire to ensure that a
        // scheduled job can run at the time of the next expiration if it is before the daily job
        // would run. This test verifies that this method returns the expected values depending on
        // when the key should expire and also verifies that the method to schedule the next job to
        // update the keystore is the expected value based on the time of the next expiration.

        final long epsilon = 5000;

        // Ensure the allowed time is set to the default.
        setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);

        // If there are no keys in the keystore the expiration time should be -1.
        assertEquals("The expiration time must be -1 when there are no keys in the keystore", -1,
                mKeyStore.getNextExpirationTime());

        // Allow the test key to connect with the 'always allow' option.
        runAdbTest(TEST_KEY_1, true, true, false);

        // Verify that the current expiration time is within a small value of the default time.
        long expirationTime = mKeyStore.getNextExpirationTime();
        if (Math.abs(expirationTime - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME)
                > epsilon) {
            fail("The expiration time for a new key, " + expirationTime
                    + ", is outside the expected value of "
                    + Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
        }
        // The delay until the next job should be the lesser of the default expiration time and the
        // AdbDebuggingHandler's job interval.
        long expectedValue = Math.min(
                AdbDebuggingManager.AdbDebuggingHandler.UPDATE_KEYSTORE_JOB_INTERVAL,
                Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
        long delay = mHandler.scheduleJobToUpdateAdbKeyStore();
        if (Math.abs(delay - expectedValue) > epsilon) {
            fail("The delay before the next scheduled job, " + delay
                    + ", is outside the expected value of " + expectedValue);
        }

        // Set the current expiration time to a minute from expiration and verify this new value is
        // returned.
        final long newExpirationTime = 60000;
        mKeyStore.setLastConnectionTime(TEST_KEY_1,
                System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
                        + newExpirationTime, true);
        expirationTime = mKeyStore.getNextExpirationTime();
        if (Math.abs(expirationTime - newExpirationTime) > epsilon) {
            fail("The expiration time for a key about to expire, " + expirationTime
                    + ", is outside the expected value of " + newExpirationTime);
        }
        delay = mHandler.scheduleJobToUpdateAdbKeyStore();
        if (Math.abs(delay - newExpirationTime) > epsilon) {
            fail("The delay before the next scheduled job, " + delay
                    + ", is outside the expected value of " + newExpirationTime);
        }

        // If a key is already expired the expiration time and delay before the next job runs should
        // be 0.
        mKeyStore.setLastConnectionTime(TEST_KEY_1, 1, true);
        assertEquals("The expiration time for a key that is already expired must be 0", 0,
                mKeyStore.getNextExpirationTime());
        assertEquals(
                "The delay before the next scheduled job for a key that is already expired must"
                        + " be 0", 0, mHandler.scheduleJobToUpdateAdbKeyStore());

        // If the previous behavior of never removing old keys is set then the expiration time
        // should be -1 to indicate the job does not need to run.
        setAllowedConnectionTime(0);
        assertEquals("The expiration time must be -1 when the keys are set to never expire", -1,
                mKeyStore.getNextExpirationTime());
    }

    @Test
    public void testConnectionTimeUpdatedWithConnectedKeyMessage() throws Exception {
        // When a system successfully passes the SIGNATURE challenge adbd sends a connected key
        // message to the framework to notify of the newly connected key. This message should
        // trigger the AdbDebuggingManager to update the last connection time for this key and mark
        // it as the currently connected key so that its time can be updated during subsequent
        // keystore update jobs as well as when the disconnected message is received.

        // Allow the test key to connect with the 'always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);

        // Simulate disconnecting the key before a subsequent connection without user interaction.
        disconnectKey(TEST_KEY_1);

        // Get the last connection time for the key to verify that it is updated when the connected
        // key message is sent.
        long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
        Thread.sleep(10);
        mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY,
                TEST_KEY_1).sendToTarget();
        flushHandlerQueue();
        assertNotEquals(
                "The connection time for the key must be updated when the connected key message "
                        + "is received",
                connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));

        // Verify that the scheduled job updates the connection time of the key.
        connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
        Thread.sleep(10);
        updateKeyStore();
        assertNotEquals(
                "The connection time for the key must be updated when the update keystore message"
                        + " is sent",
                connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));

        // Verify that the connection time is updated when the key is disconnected.
        connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
        Thread.sleep(10);
        disconnectKey(TEST_KEY_1);
        assertNotEquals(
                "The connection time for the key must be updated when the disconnected message is"
                        + " received",
                connectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
    }

    @Test
    public void testClearAuthorizations() throws Exception {
        // When the user selects the 'Revoke USB debugging authorizations' all previously 'always
        // allow' keys should be deleted.

        // Set the allowed connection time to the default value to ensure tests do not fail due to
        // a small value.
        setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);

        // Allow the test key to connect with the 'always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);
        persistKeyStore();

        // Verify that the key is authorized and in the adb_keys file
        assertTrue(
                "The test key must be in the keystore after the 'always allow' option is selected",
                mKeyStore.isKeyAuthorized(TEST_KEY_1));
        assertTrue(
                "The test key must be in the adb_keys file after the 'always allow option is "
                        + "selected",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));

        // Send the message to clear the adb authorizations and verify that the keys are no longer
        // authorized.
        clearKeyStore();
        assertFalse(
                "The test key must not be in the keystore after clearing the authorizations",
                mKeyStore.isKeyAuthorized(TEST_KEY_1));
        assertFalse(
                "The test key must not be in the adb_keys file after clearing the authorizations",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testClearKeystoreAfterDisablingAdb() throws Exception {
        // When the user disables adb they should still be able to clear the authorized keys.

        // Allow the test key to connect with the 'always allow' option selected and persist the
        // keystore.
        runAdbTest(TEST_KEY_1, true, true, false);
        persistKeyStore();

        // Disable adb and verify that the keystore can be cleared without throwing an exception.
        disableAdb();
        clearKeyStore();
        assertFalse(
                "The test key must not be in the adb_keys file after clearing the authorizations",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
    }

    @Test
    public void testUntrackedUserKeysAddedToKeystore() throws Exception {
        // When a device is first updated to a build that tracks the connection time of adb keys
        // the keys in the user key file will not have a connection time. To prevent immediately
        // deleting keys that the user is actively using these untracked keys should be added to the
        // keystore with the current system time; this gives the user time to reconnect
        // automatically with an active key while inactive keys are deleted after the expiration
        // time.

        final long epsilon = 5000;
        final String[] testKeys = {TEST_KEY_1, TEST_KEY_2};

        // Add the test keys to the user key file.
        FileOutputStream fo = new FileOutputStream(mAdbKeyFile);
        for (String key : testKeys) {
            fo.write(key.getBytes());
            fo.write('\n');
        }
        fo.close();

        // Set the expiration time to the default and use this value to verify the expiration time
        // of the previously untracked keys.
        setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);

        // The untracked keys should be added to the keystore as part of the constructor.
        AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);

        // Verify that the connection time for each test key is within a small value of the current
        // time.
        long time = System.currentTimeMillis();
        for (String key : testKeys) {
            long connectionTime = adbKeyStore.getLastConnectionTime(key);
            if (Math.abs(connectionTime - connectionTime) > epsilon) {
                fail("The connection time for a previously untracked key, " + connectionTime
                        + ", is beyond the current time of " + time);
            }
        }
    }

    @Test
    public void testConnectionTimeUpdatedForMultipleConnectedKeys() throws Exception {
        // Since ADB supports multiple simultaneous connections verify that the connection time of
        // each key is updated by the scheduled job as long as it is connected.

        // Allow both test keys to connect with the 'always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);
        runAdbTest(TEST_KEY_2, true, true, false);

        // Sleep a small amount of time to ensure the connection time is updated by the scheduled
        // job.
        long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
        long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
        Thread.sleep(10);
        updateKeyStore();
        assertNotEquals(
                "The connection time for test key 1 must be updated after the scheduled job runs",
                connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
        assertNotEquals(
                "The connection time for test key 2 must be updated after the scheduled job runs",
                connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));

        // Disconnect the second test key and verify that the last connection time of the first key
        // is the only one updated.
        disconnectKey(TEST_KEY_2);
        connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
        connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
        Thread.sleep(10);
        updateKeyStore();
        assertNotEquals(
                "The connection time for test key 1 must be updated after another key is "
                        + "disconnected and the scheduled job runs",
                connectionTime1, mKeyStore.getLastConnectionTime(TEST_KEY_1));
        assertEquals(
                "The connection time for test key 2 must not be updated after it is disconnected",
                connectionTime2, mKeyStore.getLastConnectionTime(TEST_KEY_2));
    }

    @Test
    public void testClearAuthorizationsBeforeAdbEnabled() throws Exception {
        // The adb key store is not instantiated until adb is enabled; however if the user attempts
        // to clear the adb authorizations when adb is disabled after a boot a NullPointerException
        // was thrown as deleteKeyStore is invoked against the key store. This test ensures the
        // key store can be successfully cleared when adb is disabled.
        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper());

        clearKeyStore();
    }

    @Test
    public void testClearAuthorizationsDeletesKeyFiles() throws Exception {
        mAdbKeyFile.createNewFile();
        mAdbKeyXmlFile.createNewFile();

        clearKeyStore();

        assertFalse("The adb key file should have been deleted after revocation of the grants",
                mAdbKeyFile.exists());
        assertFalse("The adb xml key file should have been deleted after revocation of the grants",
                mAdbKeyXmlFile.exists());
    }

    /**
     * Runs an adb test with the provided configuration.
     *
     * @param key The base64 encoding of the key to be used during the test.
     * @param allowKey boolean indicating whether the key should be allowed to connect.
     * @param alwaysAllow boolean indicating whether the 'Always allow' option should be selected.
     * @param autoAllowExpected boolean indicating whether the key is expected to be automatically
     *                          allowed without user interaction.
     */
    private void runAdbTest(String key, boolean allowKey, boolean alwaysAllow,
            boolean autoAllowExpected) throws Exception {
        // if the key should not be automatically allowed then set up the activity
        if (!autoAllowExpected) {
            new AdbDebuggingManagerTestActivity.Configurator()
                    .setExpectedKey(key)
                    .setAllowKey(allowKey)
                    .setAlwaysAllow(alwaysAllow)
                    .setHandler(mHandler)
                    .setBlockingQueue(mBlockingQueue);
        }
        // send the message indicating a new key is attempting to connect
        mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONFIRM,
                key).sendToTarget();
        // if the key should not be automatically allowed then the ADB public key confirmation
        // activity should be launched
        if (!autoAllowExpected) {
            TestResult activityResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT);
            assertNotNull(
                    "The ADB public key confirmation activity did not complete within the timeout"
                            + " period", activityResult);
            assertEquals("The ADB public key activity failed with result: " + activityResult,
                    TestResult.RESULT_ACTIVITY_LAUNCHED, activityResult.mReturnCode);
        }
        // If the activity was launched it should send a response back to the manager that would
        // trigger a response to the thread, or if the key is a known valid key then a response
        // should be sent back without requiring interaction with the activity.
        TestResult threadResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT);
        assertNotNull("A response was not sent to the thread within the timeout period",
                threadResult);
        // verify that the result is an expected message from the thread
        assertEquals("An unexpected result was received: " + threadResult,
                TestResult.RESULT_RESPONSE_RECEIVED, threadResult.mReturnCode);
        assertEquals("The manager did not send the proper response for allowKey = " + allowKey,
                allowKey ? RESPONSE_KEY_ALLOWED : RESPONSE_KEY_DENIED, threadResult.mMessage);
        // if the key is not allowed or not always allowed verify it is not in the key store
        if (!allowKey || !alwaysAllow) {
            assertFalse("The key must not be authorized in the key store",
                    mKeyStore.isKeyAuthorized(key));
            assertFalse(
                    "The key must not be stored in the adb_keys file",
                    isKeyInFile(key, mAdbKeyFile));
        }
        flushHandlerQueue();
    }

    private void persistKeyStore() throws Exception {
        // Send a message to the handler to persist the key store.
        mHandler.obtainMessage(
                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE)
                    .sendToTarget();
        flushHandlerQueue();
    }

    private void disconnectKey(String key) throws Exception {
        // Send a message to the handler to disconnect the currently connected key.
        mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT,
                key).sendToTarget();
        flushHandlerQueue();
    }

    private void updateKeyStore() throws Exception {
        // Send a message to the handler to run the update keystore job.
        mHandler.obtainMessage(
                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE).sendToTarget();
        flushHandlerQueue();
    }

    private void clearKeyStore() throws Exception {
        // Send a message to the handler to clear all previously authorized keys.
        mHandler.obtainMessage(
                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
        flushHandlerQueue();
    }

    private void disableAdb() throws Exception {
        // Send a message to the handler to disable adb.
        mHandler.obtainMessage(
                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISABLED).sendToTarget();
        flushHandlerQueue();
    }

    private void flushHandlerQueue() throws Exception {
        // Post a Runnable to ensure that all of the current messages in the queue are flushed.
        CountDownLatch latch = new CountDownLatch(1);
        mHandler.post(() -> {
            latch.countDown();
        });
        if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
            fail("The Runnable to flush the handler's queue did not complete within the timeout "
                    + "period");
        }
    }

    private boolean isKeyInFile(String key, File keyFile) throws Exception {
        if (key == null) {
            return false;
        }
        if (keyFile.exists()) {
            try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) {
                String currKey;
                while ((currKey = in.readLine()) != null) {
                    if (key.equals(currKey)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager
     * indicating whether the key should be allowed to  connect.
     */
    class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
        AdbDebuggingThreadTest() {
            mManager.super();
        }

        @Override
        public void sendResponse(String msg) {
            TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg);
            try {
                mBlockingQueue.put(result);
            } catch (InterruptedException e) {
                Log.e(TAG,
                        "Caught an InterruptedException putting the result in the queue: " + result,
                        e);
            }
        }
    }

    /**
     * Contains the result for the current portion of the test along with any corresponding
     * messages.
     */
    public static class TestResult {
        public int mReturnCode;
        public String mMessage;

        public static final int RESULT_ACTIVITY_LAUNCHED = 1;
        public static final int RESULT_UNEXPECTED_KEY = 2;
        public static final int RESULT_RESPONSE_RECEIVED = 3;

        public TestResult(int returnCode) {
            this(returnCode, null);
        }

        public TestResult(int returnCode, String message) {
            mReturnCode = returnCode;
            mMessage = message;
        }

        @Override
        public String toString() {
            return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}";
        }
    }
}
