Remove keys from adb_keys after period of inactivity

Previously the feature to revoke 'always allow' adb grants
after a period of activity would not use the adb_keys file
and instead would just check the last connection time of
an 'always allow' key to determine if it should be allowed
without user interaction. However this bypassed the adbd
SIGNATURE check the verifies the system possesses the
corresponding private key. This change resolves this by
writing 'always allow' keys to the adb_keys file and
running a periodic job to remove any keys from this file
that have not been used within the expiration window. This
change also adds support for a 'connected key' message
from adbd so that the framework can be notified when a
key passes the SIGNATURE check.

Bug: 124076524
Test: atest AdbDebuggingManagerTest
Change-Id: I4c252f4ddd77f56a30b807d645cdab12a03d9bc5
diff --git a/core/proto/android/service/adb.proto b/core/proto/android/service/adb.proto
index 493f9b8..549d30c 100644
--- a/core/proto/android/service/adb.proto
+++ b/core/proto/android/service/adb.proto
@@ -35,4 +35,5 @@
     optional string last_key_recevied = 2 [ (android.privacy).dest = DEST_EXPLICIT ];
     optional string user_keys = 3 [ (android.privacy).dest = DEST_LOCAL ];
     optional string system_keys = 4 [ (android.privacy).dest = DEST_LOCAL ];
+    optional string keystore = 5 [ (android.privacy).dest = DEST_LOCAL ];
 }
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 9325d25..bdbff3d 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -27,9 +27,11 @@
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.debug.AdbProtoEnums;
 import android.net.LocalSocket;
 import android.net.LocalSocketAddress;
+import android.net.Uri;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -48,6 +50,7 @@
 import android.util.Xml;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
@@ -57,18 +60,24 @@
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi
@@ -85,19 +94,22 @@
     // This file contains keys that will be allowed to connect without user interaction as long
     // as a subsequent connection occurs within the allowed duration.
     private static final String ADB_TEMP_KEYS_FILE = "adb_temp_keys.xml";
-    private static final int BUFFER_SIZE = 4096;
+    private static final int BUFFER_SIZE = 65536;
 
     private final Context mContext;
     private final Handler mHandler;
     private AdbDebuggingThread mThread;
     private boolean mAdbEnabled = false;
     private String mFingerprints;
-    private String mConnectedKey;
+    private final List<String> mConnectedKeys;
     private String mConfirmComponent;
+    private final File mTestUserKeyFile;
 
     public AdbDebuggingManager(Context context) {
         mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
         mContext = context;
+        mTestUserKeyFile = null;
+        mConnectedKeys = new ArrayList<>(1);
     }
 
     /**
@@ -105,10 +117,12 @@
      * an adb connection from the key.
      */
     @TestApi
-    protected AdbDebuggingManager(Context context, String confirmComponent) {
+    protected AdbDebuggingManager(Context context, String confirmComponent, File testUserKeyFile) {
         mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
         mContext = context;
         mConfirmComponent = confirmComponent;
+        mTestUserKeyFile = testUserKeyFile;
+        mConnectedKeys = new ArrayList<>();
     }
 
     class AdbDebuggingThread extends Thread {
@@ -153,12 +167,13 @@
                 mInputStream = null;
 
                 if (DEBUG) Slog.d(TAG, "Creating socket");
-                mSocket = new LocalSocket();
+                mSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
                 mSocket.connect(address);
 
                 mOutputStream = mSocket.getOutputStream();
                 mInputStream = mSocket.getInputStream();
             } catch (IOException ioe) {
+                Slog.e(TAG, "Caught an exception opening the socket: " + ioe);
                 closeSocketLocked();
                 throw ioe;
             }
@@ -172,6 +187,7 @@
                     // if less than 2 bytes are read the if statements below will throw an
                     // IndexOutOfBoundsException.
                     if (count < 2) {
+                        Slog.w(TAG, "Read failed with count " + count);
                         break;
                     }
 
@@ -183,9 +199,18 @@
                         msg.obj = key;
                         mHandler.sendMessage(msg);
                     } else if (buffer[0] == 'D' && buffer[1] == 'C') {
-                        Slog.d(TAG, "Received disconnected message");
+                        String key = new String(Arrays.copyOfRange(buffer, 2, count));
+                        Slog.d(TAG, "Received disconnected message: " + key);
                         Message msg = mHandler.obtainMessage(
                                 AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT);
+                        msg.obj = key;
+                        mHandler.sendMessage(msg);
+                    } else if (buffer[0] == 'C' && buffer[1] == 'K') {
+                        String key = new String(Arrays.copyOfRange(buffer, 2, count));
+                        Slog.d(TAG, "Received connected key message: " + key);
+                        Message msg = mHandler.obtainMessage(
+                                AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY);
+                        msg.obj = key;
                         mHandler.sendMessage(msg);
                     } else {
                         Slog.e(TAG, "Wrong message: "
@@ -243,14 +268,13 @@
     }
 
     class AdbDebuggingHandler extends Handler {
-        // The time to schedule the job to keep the key store updated with a currently connected
-        // key. This job is required because a deveoper could keep a device connected to their
-        // system beyond the time within which a subsequent connection is allowed. But since the
-        // last connection time is only written when a device is connected and disconnected then
-        // if the device is rebooted while connected to the development system it would appear as
-        // though the adb grant for the system is no longer authorized and the developer would need
-        // to manually allow the connection again.
-        private static final long UPDATE_KEY_STORE_JOB_INTERVAL = 86400000;
+        // The default time to schedule the job to keep the keystore updated with a currently
+        // connected key as well as to removed expired keys.
+        static final long UPDATE_KEYSTORE_JOB_INTERVAL = 86400000;
+        // The minimum interval at which the job should run to update the keystore. This is intended
+        // to prevent the job from running too often if the allowed connection time for adb grants
+        // is set to an extremely small value.
+        static final long UPDATE_KEYSTORE_MIN_JOB_INTERVAL = 60000;
 
         static final int MESSAGE_ADB_ENABLED = 1;
         static final int MESSAGE_ADB_DISABLED = 2;
@@ -259,11 +283,21 @@
         static final int MESSAGE_ADB_CONFIRM = 5;
         static final int MESSAGE_ADB_CLEAR = 6;
         static final int MESSAGE_ADB_DISCONNECT = 7;
-        static final int MESSAGE_ADB_PERSIST_KEY_STORE = 8;
-        static final int MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME = 9;
+        static final int MESSAGE_ADB_PERSIST_KEYSTORE = 8;
+        static final int MESSAGE_ADB_UPDATE_KEYSTORE = 9;
+        static final int MESSAGE_ADB_CONNECTED_KEY = 10;
 
         private AdbKeyStore mAdbKeyStore;
 
+        private ContentObserver mAuthTimeObserver = new ContentObserver(this) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                Slog.d(TAG, "Received notification that uri " + uri
+                        + " was modified; rescheduling keystore job");
+                scheduleJobToUpdateAdbKeyStore();
+            }
+        };
+
         AdbDebuggingHandler(Looper looper) {
             super(looper);
         }
@@ -285,13 +319,15 @@
                     if (mAdbEnabled) {
                         break;
                     }
-
+                    registerForAuthTimeChanges();
                     mAdbEnabled = true;
 
                     mThread = new AdbDebuggingThread();
                     mThread.start();
 
                     mAdbKeyStore = new AdbKeyStore();
+                    mAdbKeyStore.updateKeyStore();
+                    scheduleJobToUpdateAdbKeyStore();
                     break;
 
                 case MESSAGE_ADB_DISABLED:
@@ -306,14 +342,20 @@
                         mThread = null;
                     }
 
-                    cancelJobToUpdateAdbKeyStore();
-                    mConnectedKey = null;
+                    if (!mConnectedKeys.isEmpty()) {
+                        for (String connectedKey : mConnectedKeys) {
+                            mAdbKeyStore.setLastConnectionTime(connectedKey,
+                                    System.currentTimeMillis());
+                        }
+                        sendPersistKeyStoreMessage();
+                        mConnectedKeys.clear();
+                    }
+                    scheduleJobToUpdateAdbKeyStore();
                     break;
 
                 case MESSAGE_ADB_ALLOW: {
                     String key = (String) msg.obj;
                     String fingerprints = getFingerprints(key);
-
                     if (!fingerprints.equals(mFingerprints)) {
                         Slog.e(TAG, "Fingerprints do not match. Got "
                                 + fingerprints + ", expected " + mFingerprints);
@@ -324,12 +366,12 @@
                     if (mThread != null) {
                         mThread.sendResponse("OK");
                         if (alwaysAllow) {
-                            mConnectedKey = key;
+                            if (!mConnectedKeys.contains(key)) {
+                                mConnectedKeys.add(key);
+                            }
                             mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                            sendPersistKeyStoreMessage();
                             scheduleJobToUpdateAdbKeyStore();
-                            // write this key to adb_keys as well so that subsequent connections can
-                            // go through the expected SIGNATURE interaction.
-                            writeKey(key);
                         }
                         logAdbConnectionChanged(key, AdbProtoEnums.USER_ALLOWED, alwaysAllow);
                     }
@@ -369,47 +411,129 @@
                 }
 
                 case MESSAGE_ADB_CLEAR: {
-                    deleteKeyFile();
-                    mConnectedKey = null;
+                    Slog.d(TAG, "Received a request to clear the adb authorizations");
+                    mConnectedKeys.clear();
                     mAdbKeyStore.deleteKeyStore();
                     cancelJobToUpdateAdbKeyStore();
                     break;
                 }
 
                 case MESSAGE_ADB_DISCONNECT: {
-                    if (mConnectedKey != null) {
-                        mAdbKeyStore.setLastConnectionTime(mConnectedKey,
-                                System.currentTimeMillis());
-                        cancelJobToUpdateAdbKeyStore();
+                    String key = (String) msg.obj;
+                    boolean alwaysAllow = false;
+                    if (key != null && key.length() > 0) {
+                        if (mConnectedKeys.contains(key)) {
+                            alwaysAllow = true;
+                            mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                            sendPersistKeyStoreMessage();
+                            scheduleJobToUpdateAdbKeyStore();
+                            mConnectedKeys.remove(key);
+                        }
+                    } else {
+                        Slog.w(TAG, "Received a disconnected key message with an empty key");
                     }
-                    logAdbConnectionChanged(mConnectedKey, AdbProtoEnums.DISCONNECTED,
-                            (mConnectedKey != null));
-                    mConnectedKey = null;
+                    logAdbConnectionChanged(key, AdbProtoEnums.DISCONNECTED, alwaysAllow);
                     break;
                 }
 
-                case MESSAGE_ADB_PERSIST_KEY_STORE: {
-                    mAdbKeyStore.persistKeyStore();
+                case MESSAGE_ADB_PERSIST_KEYSTORE: {
+                    if (mAdbKeyStore != null) {
+                        mAdbKeyStore.persistKeyStore();
+                    }
                     break;
                 }
 
-                case MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME: {
-                    if (mConnectedKey != null) {
-                        mAdbKeyStore.setLastConnectionTime(mConnectedKey,
-                                System.currentTimeMillis());
+                case MESSAGE_ADB_UPDATE_KEYSTORE: {
+                    if (!mConnectedKeys.isEmpty()) {
+                        for (String connectedKey : mConnectedKeys) {
+                            mAdbKeyStore.setLastConnectionTime(connectedKey,
+                                    System.currentTimeMillis());
+                        }
+                        sendPersistKeyStoreMessage();
                         scheduleJobToUpdateAdbKeyStore();
+                    } else if (!mAdbKeyStore.isEmpty()) {
+                        mAdbKeyStore.updateKeyStore();
+                        scheduleJobToUpdateAdbKeyStore();
+                    }
+                    break;
+                }
+
+                case MESSAGE_ADB_CONNECTED_KEY: {
+                    String key = (String) msg.obj;
+                    if (key == null || key.length() == 0) {
+                        Slog.w(TAG, "Received a connected key message with an empty key");
+                    } else {
+                        if (!mConnectedKeys.contains(key)) {
+                            mConnectedKeys.add(key);
+                        }
+                        mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                        sendPersistKeyStoreMessage();
+                        scheduleJobToUpdateAdbKeyStore();
+                        logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true);
                     }
                     break;
                 }
             }
         }
 
+        void registerForAuthTimeChanges() {
+            Uri uri = Settings.Global.getUriFor(Settings.Global.ADB_ALLOWED_CONNECTION_TIME);
+            mContext.getContentResolver().registerContentObserver(uri, false, mAuthTimeObserver);
+        }
+
         private void logAdbConnectionChanged(String key, int state, boolean alwaysAllow) {
             long lastConnectionTime = mAdbKeyStore.getLastConnectionTime(key);
             long authWindow = mAdbKeyStore.getAllowedConnectionTime();
+            Slog.d(TAG,
+                    "Logging key " + key + ", state = " + state + ", alwaysAllow = " + alwaysAllow
+                            + ", lastConnectionTime = " + lastConnectionTime + ", authWindow = "
+                            + authWindow);
             StatsLog.write(StatsLog.ADB_CONNECTION_CHANGED, lastConnectionTime, authWindow, state,
                     alwaysAllow);
         }
+
+
+        /**
+         * Schedules a job to update the connection time of the currently connected key and filter
+         * out any keys that are beyond their expiration time.
+         *
+         * @return the time in ms when the next job will run or -1 if the job should not be
+         * scheduled to run.
+         */
+        @VisibleForTesting
+        long scheduleJobToUpdateAdbKeyStore() {
+            cancelJobToUpdateAdbKeyStore();
+            long keyExpiration = mAdbKeyStore.getNextExpirationTime();
+            // if the keyExpiration time is -1 then either the keys are set to never expire or
+            // there are no keys in the keystore, just return for now as a new job will be
+            // scheduled on the next connection or when the auth time changes.
+            if (keyExpiration == -1) {
+                return -1;
+            }
+            long delay;
+            // if the keyExpiration is 0 this indicates a key has already expired; schedule the job
+            // to run now to ensure the key is removed immediately from adb_keys.
+            if (keyExpiration == 0) {
+                delay = 0;
+            } else {
+                // else the next job should be run either daily or when the next key is set to
+                // expire with a min job interval to ensure this job does not run too often if a
+                // small value is set for the key expiration.
+                delay = Math.max(Math.min(UPDATE_KEYSTORE_JOB_INTERVAL, keyExpiration),
+                        UPDATE_KEYSTORE_MIN_JOB_INTERVAL);
+            }
+            Message message = obtainMessage(MESSAGE_ADB_UPDATE_KEYSTORE);
+            sendMessageDelayed(message, delay);
+            return delay;
+        }
+
+        /**
+         * Cancels the scheduled job to update the connection time of the currently connected key
+         * and to remove any expired keys.
+         */
+        private void cancelJobToUpdateAdbKeyStore() {
+            removeMessages(AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEYSTORE);
+        }
     }
 
     private String getFingerprints(String key) {
@@ -534,7 +658,13 @@
     }
 
     File getUserKeyFile() {
-        return getAdbFile(ADB_KEYS_FILE);
+        return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile;
+    }
+
+    private void createKeyFile(File keyFile) throws IOException {
+        keyFile.createNewFile();
+        FileUtils.setPermissions(keyFile.toString(),
+                FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
     }
 
     private void writeKey(String key) {
@@ -546,9 +676,7 @@
             }
 
             if (!keyFile.exists()) {
-                keyFile.createNewFile();
-                FileUtils.setPermissions(keyFile.toString(),
-                        FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
+                createKeyFile(keyFile);
             }
 
             FileOutputStream fo = new FileOutputStream(keyFile, true);
@@ -560,6 +688,35 @@
         }
     }
 
+    private void writeKeys(Iterable<String> keys) {
+        AtomicFile atomicKeyFile = null;
+        FileOutputStream fo = null;
+        try {
+            File keyFile = getUserKeyFile();
+
+            if (keyFile == null) {
+                return;
+            }
+
+            if (!keyFile.exists()) {
+                createKeyFile(keyFile);
+            }
+
+            atomicKeyFile = new AtomicFile(keyFile);
+            fo = atomicKeyFile.startWrite();
+            for (String key : keys) {
+                fo.write(key.getBytes());
+                fo.write('\n');
+            }
+            atomicKeyFile.finishWrite(fo);
+        } catch (IOException ex) {
+            Slog.e(TAG, "Error writing keys: " + ex);
+            if (atomicKeyFile != null) {
+                atomicKeyFile.failWrite(fo);
+            }
+        }
+    }
+
     private void deleteKeyFile() {
         File keyFile = getUserKeyFile();
         if (keyFile != null) {
@@ -604,36 +761,14 @@
     }
 
     /**
-     * Sends a message to the handler to persist the key store.
+     * Sends a message to the handler to persist the keystore.
      */
     private void sendPersistKeyStoreMessage() {
-        Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE);
+        Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEYSTORE);
         mHandler.sendMessage(msg);
     }
 
     /**
-     * Schedules a job to update the connection time of the currently connected key. This is
-     * intended for cases such as development devices that are left connected to a user's
-     * system beyond the window within which a connection is allowed without user interaction.
-     * A job should be rescheduled daily so that if the device is rebooted while connected to
-     * the user's system the last time in the key store will show within 24 hours which should
-     * be within the allowed window.
-     */
-    private void scheduleJobToUpdateAdbKeyStore() {
-        Message message = mHandler.obtainMessage(
-                AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME);
-        mHandler.sendMessageDelayed(message, AdbDebuggingHandler.UPDATE_KEY_STORE_JOB_INTERVAL);
-    }
-
-    /**
-     * Cancels the scheduled job to update the connection time of the currently connected key.
-     * This should be invoked once the adb session is disconnected.
-     */
-    private void cancelJobToUpdateAdbKeyStore() {
-        mHandler.removeMessages(AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME);
-    }
-
-    /**
      * Dump the USB debugging state.
      */
     public void dump(DualDumpOutputStream dump, String idName, long id) {
@@ -657,6 +792,13 @@
             Slog.e(TAG, "Cannot read system keys", e);
         }
 
+        try {
+            dump.write("keystore", AdbDebuggingManagerProto.KEYSTORE,
+                    FileUtils.readTextFile(getAdbTempKeysFile(), 0, null));
+        } catch (IOException e) {
+            Slog.e(TAG, "Cannot read keystore: ", e);
+        }
+
         dump.end(token);
     }
 
@@ -667,11 +809,14 @@
      */
     class AdbKeyStore {
         private Map<String, Long> mKeyMap;
+        private Set<String> mSystemKeys;
         private File mKeyFile;
         private AtomicFile mAtomicKeyFile;
+
         private static final String XML_TAG_ADB_KEY = "adbKey";
         private static final String XML_ATTRIBUTE_KEY = "key";
         private static final String XML_ATTRIBUTE_LAST_CONNECTION = "lastConnection";
+        private static final String SYSTEM_KEY_FILE = "/adb_keys";
 
         /**
          * Value returned by {@code getLastConnectionTime} when there is no previously saved
@@ -680,21 +825,25 @@
         public static final long NO_PREVIOUS_CONNECTION = 0;
 
         /**
-         * Constructor that uses the default location for the persistent adb key store.
+         * Constructor that uses the default location for the persistent adb keystore.
          */
         AdbKeyStore() {
-            initKeyFile();
-            mKeyMap = getKeyMapFromFile();
+            init();
         }
 
         /**
-         * Constructor that uses the specified file as the location for the persistent adb key
-         * store.
+         * Constructor that uses the specified file as the location for the persistent adb keystore.
          */
         AdbKeyStore(File keyFile) {
             mKeyFile = keyFile;
+            init();
+        }
+
+        private void init() {
             initKeyFile();
-            mKeyMap = getKeyMapFromFile();
+            mKeyMap = getKeyMap();
+            mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE);
+            addUserKeysToKeyStore();
         }
 
         /**
@@ -710,10 +859,46 @@
             }
         }
 
+        private Set<String> getSystemKeysFromFile(String fileName) {
+            Set<String> systemKeys = new HashSet<>();
+            File systemKeyFile = new File(fileName);
+            if (systemKeyFile.exists()) {
+                try (BufferedReader in = new BufferedReader(new FileReader(systemKeyFile))) {
+                    String key;
+                    while ((key = in.readLine()) != null) {
+                        key = key.trim();
+                        if (key.length() > 0) {
+                            systemKeys.add(key);
+                        }
+                    }
+                } catch (IOException e) {
+                    Slog.e(TAG, "Caught an exception reading " + fileName + ": " + e);
+                }
+            }
+            return systemKeys;
+        }
+
+        /**
+         * Returns whether there are any 'always allowed' keys in the keystore.
+         */
+        public boolean isEmpty() {
+            return mKeyMap.isEmpty();
+        }
+
+        /**
+         * Iterates through the keys in the keystore and removes any that are beyond the window
+         * within which connections are automatically allowed without user interaction.
+         */
+        public void updateKeyStore() {
+            if (filterOutOldKeys()) {
+                sendPersistKeyStoreMessage();
+            }
+        }
+
         /**
          * Returns the key map with the keys and last connection times from the key file.
          */
-        private Map<String, Long> getKeyMapFromFile() {
+        private Map<String, Long> getKeyMap() {
             Map<String, Long> keyMap = new HashMap<String, Long>();
             // if the AtomicFile could not be instantiated before attempt again; if it still fails
             // return an empty key map.
@@ -760,10 +945,39 @@
         }
 
         /**
+         * Updates the keystore with keys that were previously set to be always allowed before the
+         * connection time of keys was tracked.
+         */
+        private void addUserKeysToKeyStore() {
+            File userKeyFile = getUserKeyFile();
+            boolean mapUpdated = false;
+            if (userKeyFile != null && userKeyFile.exists()) {
+                try (BufferedReader in = new BufferedReader(new FileReader(userKeyFile))) {
+                    long time = System.currentTimeMillis();
+                    String key;
+                    while ((key = in.readLine()) != null) {
+                        // if the keystore does not contain the key from the user key file then add
+                        // it to the Map with the current system time to prevent it from expiring
+                        // immediately if the user is actively using this key.
+                        if (!mKeyMap.containsKey(key)) {
+                            mKeyMap.put(key, time);
+                            mapUpdated = true;
+                        }
+                    }
+                } catch (IOException e) {
+                    Slog.e(TAG, "Caught an exception reading " + userKeyFile + ": " + e);
+                }
+            }
+            if (mapUpdated) {
+                sendPersistKeyStoreMessage();
+            }
+        }
+
+        /**
          * Writes the key map to the key file.
          */
         public void persistKeyStore() {
-            // if there is nothing in the key map then ensure any keys left in the key store files
+            // if there is nothing in the key map then ensure any keys left in the keystore files
             // are deleted as well.
             filterOutOldKeys();
             if (mKeyMap.isEmpty()) {
@@ -800,7 +1014,8 @@
             }
         }
 
-        private void filterOutOldKeys() {
+        private boolean filterOutOldKeys() {
+            boolean keysDeleted = false;
             long allowedTime = getAllowedConnectionTime();
             long systemTime = System.currentTimeMillis();
             Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
@@ -809,8 +1024,41 @@
                 long connectionTime = keyEntry.getValue();
                 if (allowedTime != 0 && systemTime > (connectionTime + allowedTime)) {
                     keyMapIterator.remove();
+                    keysDeleted = true;
                 }
             }
+            // if any keys were deleted then the key file should be rewritten with the active keys
+            // to prevent authorizing a key that is now beyond the allowed window.
+            if (keysDeleted) {
+                writeKeys(mKeyMap.keySet());
+            }
+            return keysDeleted;
+        }
+
+        /**
+         * Returns the time in ms that the next key will expire or -1 if there are no keys or the
+         * keys will not expire.
+         */
+        public long getNextExpirationTime() {
+            long minExpiration = -1;
+            long allowedTime = getAllowedConnectionTime();
+            // if the allowedTime is 0 then keys never expire; return -1 to indicate this
+            if (allowedTime == 0) {
+                return minExpiration;
+            }
+            long systemTime = System.currentTimeMillis();
+            Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
+            while (keyMapIterator.hasNext()) {
+                Map.Entry<String, Long> keyEntry = keyMapIterator.next();
+                long connectionTime = keyEntry.getValue();
+                // if the key has already expired then ensure that the result is set to 0 so that
+                // any scheduled jobs to clean up the keystore can run right away.
+                long keyExpiration = Math.max(0, (connectionTime + allowedTime) - systemTime);
+                if (minExpiration == -1 || keyExpiration < minExpiration) {
+                    minExpiration = keyExpiration;
+                }
+            }
+            return minExpiration;
         }
 
         /**
@@ -818,6 +1066,7 @@
          */
         public void deleteKeyStore() {
             mKeyMap.clear();
+            deleteKeyFile();
             if (mAtomicKeyFile == null) {
                 return;
             }
@@ -836,37 +1085,31 @@
          * Sets the time of the last connection for the specified key to the provided time.
          */
         public void setLastConnectionTime(String key, long connectionTime) {
-            // Do not set the connection time to a value that is earlier than what was previously
-            // stored as the last connection time.
-            if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime) {
-                return;
-            }
-            mKeyMap.put(key, connectionTime);
-            sendPersistKeyStoreMessage();
+            setLastConnectionTime(key, connectionTime, false);
         }
 
         /**
-         * Returns whether the specified key should be authroized to connect without user
-         * interaction. This requires that the user previously connected this device and selected
-         * the option to 'Always allow', and the time since the last connection is within the
-         * allowed window.
+         * Sets the time of the last connection for the specified key to the provided time. If force
+         * is set to true the time will be set even if it is older than the previously written
+         * connection time.
          */
-        public boolean isKeyAuthorized(String key) {
-            long lastConnectionTime = getLastConnectionTime(key);
-            if (lastConnectionTime == NO_PREVIOUS_CONNECTION) {
-                return false;
+        public void setLastConnectionTime(String key, long connectionTime, boolean force) {
+            // Do not set the connection time to a value that is earlier than what was previously
+            // stored as the last connection time unless force is set.
+            if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime && !force) {
+                return;
             }
-            long allowedConnectionTime = getAllowedConnectionTime();
-            // if the allowed connection time is 0 then revert to the previous behavior of always
-            // allowing previously granted adb grants.
-            if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
-                    + allowedConnectionTime))) {
-                return true;
-            } else {
-                // since this key is no longer auhorized remove it from the Map
-                removeKey(key);
-                return false;
+            // System keys are always allowed so there's no need to keep track of their connection
+            // time.
+            if (mSystemKeys.contains(key)) {
+                return;
             }
+            // if this is the first time the key is being added then write it to the key file as
+            // well.
+            if (!mKeyMap.containsKey(key)) {
+                writeKey(key);
+            }
+            mKeyMap.put(key, connectionTime);
         }
 
         /**
@@ -880,14 +1123,29 @@
         }
 
         /**
-         * Removes the specified key from the key store.
+         * Returns whether the specified key should be authroized to connect without user
+         * interaction. This requires that the user previously connected this device and selected
+         * the option to 'Always allow', and the time since the last connection is within the
+         * allowed window.
          */
-        public void removeKey(String key) {
-            if (!mKeyMap.containsKey(key)) {
-                return;
+        public boolean isKeyAuthorized(String key) {
+            // A system key is always authorized to connect.
+            if (mSystemKeys.contains(key)) {
+                return true;
             }
-            mKeyMap.remove(key);
-            sendPersistKeyStoreMessage();
+            long lastConnectionTime = getLastConnectionTime(key);
+            if (lastConnectionTime == NO_PREVIOUS_CONNECTION) {
+                return false;
+            }
+            long allowedConnectionTime = getAllowedConnectionTime();
+            // if the allowed connection time is 0 then revert to the previous behavior of always
+            // allowing previously granted adb grants.
+            if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
+                    + allowedConnectionTime))) {
+                return true;
+            } else {
+                return false;
+            }
         }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
index 65af677..d4182f3 100644
--- a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
@@ -37,7 +37,10 @@
 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;
@@ -57,8 +60,8 @@
                     + "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=";
-    private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo=";
+    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";
@@ -76,22 +79,26 @@
     private AdbDebuggingManager.AdbKeyStore mKeyStore;
     private BlockingQueue<TestResult> mBlockingQueue;
     private long mOriginalAllowedConnectionTime;
-    private File mKeyFile;
+    private File mAdbKeyXmlFile;
+    private File mAdbKeyFile;
 
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getContext();
-        mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT);
-        mKeyFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
-        if (mKeyFile.exists()) {
-            mKeyFile.delete();
+        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(mKeyFile);
+        mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
         mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
         mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
         mBlockingQueue = new ArrayBlockingQueue<>(1);
-
     }
 
     @After
@@ -118,6 +125,13 @@
         // 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
@@ -146,25 +160,11 @@
 
         // Send the disconnect message for the currently connected key to trigger an update of the
         // last connection time.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
-
-        // Use a latch to ensure the test does not exit untill the Runnable has been processed.
-        CountDownLatch latch = new CountDownLatch(1);
-
-        // Post a new Runnable to the handler to ensure it runs after the disconnect message is
-        // processed.
-        mHandler.post(() -> {
-            assertNotEquals(
-                    "The last connection time was not updated after the disconnect",
-                    lastConnectionTime,
-                    mKeyStore.getLastConnectionTime(TEST_KEY_1));
-            latch.countDown();
-        });
-        if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
-            fail("The Runnable to verify the last connection time was updated did not complete "
-                    + "within the timeout period");
-        }
+        disconnectKey(TEST_KEY_1);
+        assertNotEquals(
+                "The last connection time was not updated after the disconnect",
+                lastConnectionTime,
+                mKeyStore.getLastConnectionTime(TEST_KEY_1));
     }
 
     @Test
@@ -177,8 +177,7 @@
         runAdbTest(TEST_KEY_1, true, false, false);
 
         // Send the disconnect message for the currently connected key.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
+        disconnectKey(TEST_KEY_1);
 
         // Verify that the disconnected key is not automatically allowed on a subsequent connection.
         runAdbTest(TEST_KEY_1, true, false, false);
@@ -192,12 +191,13 @@
         // Allow a connection from a new key with the 'Always allow' option selected.
         runAdbTest(TEST_KEY_1, true, true, false);
 
-        // Next attempt another connection with the same key and verify that the activity to prompt
-        // the user to accept the key is not launched.
-        runAdbTest(TEST_KEY_1, true, true, true);
+        // Send a persist keystore message to force the key to be written to the adb_keys file
+        persistKeyStore();
 
-        // Verify that a different key is not automatically allowed.
-        runAdbTest(TEST_KEY_2, false, false, false);
+        // 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
@@ -215,8 +215,11 @@
         // fail the new test but would be allowed with the original behavior.
         mKeyStore.setLastConnectionTime(TEST_KEY_1, 1);
 
-        // Run the test with the key and verify that the connection is automatically allowed.
-        runAdbTest(TEST_KEY_1, true, true, true);
+        // 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
@@ -237,24 +240,11 @@
         Thread.sleep(10);
 
         // Send a message to the handler to update the last connection time for the active key
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
-                .sendToTarget();
-
-        // Post a Runnable to the handler to ensure it runs after the update key connection time
-        // message is processed.
-        CountDownLatch latch = new CountDownLatch(1);
-        mHandler.post(() -> {
-            assertNotEquals(
-                    "The last connection time of the key was not updated after the update key "
-                            + "connection time message",
-                    lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1));
-            latch.countDown();
-        });
-        if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
-            fail("The Runnable to verify the last connection time was updated did not complete "
-                    + "within the timeout period");
-        }
+        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
@@ -266,16 +256,12 @@
         // 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.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
-                .sendToTarget();
-
-        // Post a Runnable to the handler to ensure the persist key store message has been processed
-        // using a new AdbKeyStore backed by the key file.
-        mHandler.post(() -> assertTrue(
+        // 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(mKeyFile).isKeyAuthorized(TEST_KEY_1)));
+                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);
@@ -284,29 +270,18 @@
         Thread.sleep(10);
 
         // Send a message to the handler to update the last connection time for the active key.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME)
-                .sendToTarget();
+        updateKeyStore();
 
-        // Persist the updated last connection time.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE)
-                .sendToTarget();
-
-        // Post a Runnable with a new key store backed by the key file to verify that the last
-        // connection time obtained above is different from the persisted updated value.
-        CountDownLatch latch = new CountDownLatch(1);
-        mHandler.post(() -> {
-            assertNotEquals(
-                    "The last connection time in the key file was not updated after the update "
-                            + "connection time message", lastConnectionTime,
-                    mManager.new AdbKeyStore(mKeyFile).getLastConnectionTime(TEST_KEY_1));
-            latch.countDown();
-        });
-        if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
-            fail("The Runnable to verify the last connection time was updated did not complete "
-                    + "within the timeout period");
-        }
+        // 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
@@ -318,28 +293,18 @@
         runAdbTest(TEST_KEY_1, true, true, false);
 
         // Send a message to the handler to clear the adb authorizations.
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget();
+        clearKeyStore();
 
         // Send a message to disconnect the currently connected key
-        mHandler.obtainMessage(
-                AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget();
+        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));
 
-        // Post a Runnable to ensure the disconnect has completed to verify the 'Always allow' key
-        // that was connected when the clear was sent requires authorization.
-        CountDownLatch latch = new CountDownLatch(1);
-        mHandler.post(() -> {
-            assertFalse(
-                    "The currently connected 'always allow' key should not be authorized after an"
-                            + " adb"
-                            + " clear message.",
-                    mKeyStore.isKeyAuthorized(TEST_KEY_1));
-            latch.countDown();
-        });
-        if (!latch.await(TIMEOUT, TIMEOUT_TIME_UNIT)) {
-            fail("The Runnable to verify the key is not authorized did not complete within the "
-                    + "timeout period");
-        }
+        // 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
@@ -357,8 +322,17 @@
         // Sleep for a small amount of time to exceed the allowed window.
         Thread.sleep(10);
 
-        // A new connection from this key should prompt the user again.
-        runAdbTest(TEST_KEY_1, true, true, false);
+        // 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
@@ -381,7 +355,7 @@
         // 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 should not be set to a value less "
+                "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));
 
@@ -389,11 +363,315 @@
         mKeyStore.setLastConnectionTime(TEST_KEY_1,
                 Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1)));
         assertEquals(
-                "The last connection time in the adb key store should not be set to a value less "
+                "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));
+    }
+
     /**
      * Runs an adb test with the provided configuration.
      *
@@ -440,10 +718,78 @@
                 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 should not be allowed automatically on subsequent connection attempts",
+            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;
     }
 
     /**