Merge "Remove keys from adb_keys after period of inactivity" into qt-dev
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;
     }
 
     /**