diff options
| author | 2019-01-23 16:47:48 +0000 | |
|---|---|---|
| committer | 2019-01-23 16:47:48 +0000 | |
| commit | d15f8b9ad589c4dd0964d240f72c09e4bdae8b0e (patch) | |
| tree | 1eaf6c9d21ef818a6dab3fe6e34303cd8ac64f41 | |
| parent | baed7690a2ac4593adce1b71dc8880a7f074ee47 (diff) | |
| parent | 9e7b06efe38977cc083964700f2459b175380c69 (diff) | |
Merge "Revoke 'always allow' adb grants after period of inactivity"
8 files changed, 1156 insertions, 18 deletions
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 59778b50bf86..60b2e259d729 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -27,6 +27,7 @@ import "frameworks/base/core/proto/android/app/job/enums.proto"; import "frameworks/base/core/proto/android/bluetooth/enums.proto"; import "frameworks/base/core/proto/android/bluetooth/hci/enums.proto"; import "frameworks/base/core/proto/android/bluetooth/hfp/enums.proto"; +import "frameworks/base/core/proto/android/debug/enums.proto"; import "frameworks/base/core/proto/android/net/networkcapabilities.proto"; import "frameworks/base/core/proto/android/os/enums.proto"; import "frameworks/base/core/proto/android/server/connectivity/data_stall_event.proto"; @@ -204,6 +205,7 @@ message Atom { SeOmapiReported se_omapi_reported = 141; BroadcastDispatchLatencyReported broadcast_dispatch_latency_reported = 142; AttentionManagerServiceResultReported attention_manager_service_result_reported = 143; + AdbConnectionChanged adb_connection_changed = 144; } // Pulled events will start at field 10000. @@ -4497,3 +4499,25 @@ message AttentionManagerServiceResultReported { } optional AttentionCheckResult attention_check_result = 1 [default = UNKNOWN]; } + +/** + * Logs when an adb connection changes state. + * + * Logged from: + * frameworks/base/services/core/java/com/android/server/adb/AdbDebuggingManager.java + */ +message AdbConnectionChanged { + // The last time this system connected via adb, or 0 if the 'always allow' option was not + // previously selected for this system. + optional int64 last_connection_time_millis = 1; + + // The time in ms within which a subsequent connection from an 'always allow' system is allowed + // to reconnect via adb without user interaction. + optional int64 auth_window_millis = 2; + + // The state of the adb connection from frameworks/base/core/proto/android/debug/enums.proto. + optional android.debug.AdbConnectionStateEnum state = 3; + + // True if the 'always allow' option was selected for this system. + optional bool always_allow = 4; +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 643c473f8417..c9bb3cf9b915 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11867,6 +11867,28 @@ public final class Settings { public static final String KEEP_PROFILE_IN_BACKGROUND = "keep_profile_in_background"; /** + * The default time in ms within which a subsequent connection from an always allowed system + * is allowed to reconnect without user interaction. + * + * @hide + */ + public static final long DEFAULT_ADB_ALLOWED_CONNECTION_TIME = 604800000; + + /** + * When the user first connects their device to a system a prompt is displayed to allow + * the adb connection with an option to 'Always allow' connections from this system. If the + * user selects this always allow option then the connection time is stored for the system. + * This setting is the time in ms within which a subsequent connection from an always + * allowed system is allowed to reconnect without user interaction. + * + * Type: long + * + * @hide + */ + public static final String ADB_ALLOWED_CONNECTION_TIME = + "adb_allowed_connection_time"; + + /** * Get the key that retrieves a bluetooth headset's priority. * @hide */ diff --git a/core/proto/android/debug/enums.proto b/core/proto/android/debug/enums.proto new file mode 100644 index 000000000000..6747bb7276b3 --- /dev/null +++ b/core/proto/android/debug/enums.proto @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto2"; +package android.debug; + +option java_outer_classname = "AdbProtoEnums"; +option java_multiple_files = true; + +/** + * adb connection state used to track adb connection changes in AdbDebuggingManager.java. + */ +enum AdbConnectionStateEnum { + UNKNOWN = 0; + + /** + * The adb connection is waiting for approval from the user. + */ + AWAITING_USER_APPROVAL = 1; + + /** + * The user allowed the adb connection from the system. + */ + USER_ALLOWED = 2; + + /** + * The user denied the adb connection from the system. + */ + USER_DENIED = 3; + + /** + * The adb connection was automatically allowed without user interaction due to the system + * being previously allowed by the user with the 'always allow' option selected, and the adb + * grant has not yet expired. + */ + AUTOMATICALLY_ALLOWED = 4; + + /** + * An empty or invalid base64 encoded key was provided to the framework; the connection was + * automatically denied. + */ + DENIED_INVALID_KEY = 5; + + /** + * vold decrypt has not yet occurred; the connection was automatically denied. + */ + DENIED_VOLD_DECRYPT = 6; + + /** + * The adb session has been disconnected. + */ + DISCONNECTED = 7; +} + diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index 87ad3d1067b2..9a1c556041c2 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -100,6 +100,7 @@ public class SettingsBackupTest { Settings.Global.ACTIVITY_MANAGER_CONSTANTS, Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED, Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, + Settings.Global.ADB_ALLOWED_CONNECTION_TIME, Settings.Global.ADB_ENABLED, Settings.Global.ADD_USERS_WHEN_LOCKED, Settings.Global.AIRPLANE_MODE_ON, diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java index ccead6c55b8c..c7b9a3cb7847 100644 --- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java +++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java @@ -18,6 +18,7 @@ package com.android.server.adb; import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull; +import android.annotation.TestApi; import android.app.ActivityManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -26,6 +27,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.debug.AdbProtoEnums; import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.os.Environment; @@ -37,21 +39,36 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.adb.AdbDebuggingManagerProto; +import android.util.AtomicFile; import android.util.Base64; import android.util.Slog; +import android.util.StatsLog; +import android.util.Xml; import com.android.internal.R; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.server.FgThread; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; /** * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi @@ -63,6 +80,7 @@ public class AdbDebuggingManager { private static final String ADBD_SOCKET = "adbd"; private static final String ADB_DIRECTORY = "misc/adb"; + // This file contains keys that will always be allowed to connect to the device via adb. private static final String ADB_KEYS_FILE = "adb_keys"; private static final int BUFFER_SIZE = 4096; @@ -71,12 +89,25 @@ public class AdbDebuggingManager { private AdbDebuggingThread mThread; private boolean mAdbEnabled = false; private String mFingerprints; + private String mConnectedKey; + private String mConfirmComponent; public AdbDebuggingManager(Context context) { mHandler = new AdbDebuggingHandler(FgThread.get().getLooper()); mContext = context; } + /** + * Constructor that accepts the component to be invoked to confirm if the user wants to allow + * an adb connection from the key. + */ + @TestApi + protected AdbDebuggingManager(Context context, String confirmComponent) { + mHandler = new AdbDebuggingHandler(FgThread.get().getLooper()); + mContext = context; + mConfirmComponent = confirmComponent; + } + class AdbDebuggingThread extends Thread { private boolean mStopped; private LocalSocket mSocket; @@ -135,7 +166,9 @@ public class AdbDebuggingManager { byte[] buffer = new byte[BUFFER_SIZE]; while (true) { int count = mInputStream.read(buffer); - if (count < 0) { + // if less than 2 bytes are read the if statements below will throw an + // IndexOutOfBoundsException. + if (count < 2) { break; } @@ -146,6 +179,11 @@ public class AdbDebuggingManager { AdbDebuggingHandler.MESSAGE_ADB_CONFIRM); msg.obj = key; mHandler.sendMessage(msg); + } else if (buffer[0] == 'D' && buffer[1] == 'C') { + Slog.d(TAG, "Received disconnected message"); + Message msg = mHandler.obtainMessage( + AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT); + mHandler.sendMessage(msg); } else { Slog.e(TAG, "Wrong message: " + (new String(Arrays.copyOfRange(buffer, 0, 2)))); @@ -202,15 +240,41 @@ public class AdbDebuggingManager { } class AdbDebuggingHandler extends Handler { - private static final int MESSAGE_ADB_ENABLED = 1; - private static final int MESSAGE_ADB_DISABLED = 2; - private static final int MESSAGE_ADB_ALLOW = 3; - private static final int MESSAGE_ADB_DENY = 4; - private static final int MESSAGE_ADB_CONFIRM = 5; - private static final int MESSAGE_ADB_CLEAR = 6; + // 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; + + static final int MESSAGE_ADB_ENABLED = 1; + static final int MESSAGE_ADB_DISABLED = 2; + static final int MESSAGE_ADB_ALLOW = 3; + static final int MESSAGE_ADB_DENY = 4; + 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; + + private AdbKeyStore mAdbKeyStore; AdbDebuggingHandler(Looper looper) { super(looper); + mAdbKeyStore = new AdbKeyStore(); + } + + /** + * Constructor that accepts the AdbDebuggingThread to which responses should be sent + * and the AdbKeyStore to be used to store the temporary grants. + */ + @TestApi + AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread, AdbKeyStore adbKeyStore) { + super(looper); + mThread = thread; + mAdbKeyStore = adbKeyStore; } public void handleMessage(Message msg) { @@ -251,12 +315,15 @@ public class AdbDebuggingManager { break; } - if (msg.arg1 == 1) { - writeKey(key); - } - + boolean alwaysAllow = msg.arg1 == 1; if (mThread != null) { mThread.sendResponse("OK"); + if (alwaysAllow) { + mConnectedKey = key; + mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); + scheduleJobToUpdateAdbKeyStore(); + } + logAdbConnectionChanged(key, AdbProtoEnums.USER_ALLOWED, alwaysAllow); } break; } @@ -264,36 +331,88 @@ public class AdbDebuggingManager { case MESSAGE_ADB_DENY: if (mThread != null) { mThread.sendResponse("NO"); + logAdbConnectionChanged(null, AdbProtoEnums.USER_DENIED, false); } break; case MESSAGE_ADB_CONFIRM: { + String key = (String) msg.obj; if ("trigger_restart_min_framework".equals( SystemProperties.get("vold.decrypt"))) { Slog.d(TAG, "Deferring adb confirmation until after vold decrypt"); if (mThread != null) { mThread.sendResponse("NO"); + logAdbConnectionChanged(key, AdbProtoEnums.DENIED_VOLD_DECRYPT, false); } break; } - String key = (String) msg.obj; String fingerprints = getFingerprints(key); if ("".equals(fingerprints)) { if (mThread != null) { mThread.sendResponse("NO"); + logAdbConnectionChanged(key, AdbProtoEnums.DENIED_INVALID_KEY, false); } break; } - mFingerprints = fingerprints; - startConfirmation(key, mFingerprints); + // Check if the key should be allowed without user interaction. + if (mAdbKeyStore.isKeyAuthorized(key)) { + if (mThread != null) { + mThread.sendResponse("OK"); + mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis()); + logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true); + mConnectedKey = key; + scheduleJobToUpdateAdbKeyStore(); + } + } else { + logAdbConnectionChanged(key, AdbProtoEnums.AWAITING_USER_APPROVAL, false); + mFingerprints = fingerprints; + startConfirmation(key, mFingerprints); + } break; } - case MESSAGE_ADB_CLEAR: + case MESSAGE_ADB_CLEAR: { deleteKeyFile(); + mConnectedKey = null; + mAdbKeyStore.deleteKeyStore(); + cancelJobToUpdateAdbKeyStore(); + break; + } + + case MESSAGE_ADB_DISCONNECT: { + if (mConnectedKey != null) { + mAdbKeyStore.setLastConnectionTime(mConnectedKey, + System.currentTimeMillis()); + cancelJobToUpdateAdbKeyStore(); + } + logAdbConnectionChanged(mConnectedKey, AdbProtoEnums.DISCONNECTED, + (mConnectedKey != null)); + mConnectedKey = null; + break; + } + + case MESSAGE_ADB_PERSIST_KEY_STORE: { + mAdbKeyStore.persistKeyStore(); break; + } + + case MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME: { + if (mConnectedKey != null) { + mAdbKeyStore.setLastConnectionTime(mConnectedKey, + System.currentTimeMillis()); + scheduleJobToUpdateAdbKeyStore(); + } + break; + } } } + + private void logAdbConnectionChanged(String key, int state, boolean alwaysAllow) { + long lastConnectionTime = mAdbKeyStore.getLastConnectionTime(key); + long authWindow = mAdbKeyStore.getAllowedConnectionTime(); + StatsLog.write(StatsLog.ADB_CONNECTION_CHANGED, lastConnectionTime, authWindow, state, + alwaysAllow); + } } private String getFingerprints(String key) { @@ -335,7 +454,8 @@ public class AdbDebuggingManager { UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId); String componentString; if (userInfo.isAdmin()) { - componentString = Resources.getSystem().getString( + componentString = mConfirmComponent != null + ? mConfirmComponent : Resources.getSystem().getString( com.android.internal.R.string.config_customAdbPublicKeyConfirmationComponent); } else { // If the current foreground user is not the admin user we send a different @@ -397,7 +517,10 @@ public class AdbDebuggingManager { return intent; } - private File getUserKeyFile() { + /** + * Returns a new File with the specified name in the adb directory. + */ + private File getAdbFile(String fileName) { File dataDir = Environment.getDataDirectory(); File adbDir = new File(dataDir, ADB_DIRECTORY); @@ -406,7 +529,11 @@ public class AdbDebuggingManager { return null; } - return new File(adbDir, ADB_KEYS_FILE); + return new File(adbDir, fileName); + } + + private File getUserKeyFile() { + return getAdbFile(ADB_KEYS_FILE); } private void writeKey(String key) { @@ -476,6 +603,36 @@ public class AdbDebuggingManager { } /** + * Sends a message to the handler to persist the key store. + */ + private void sendPersistKeyStoreMessage() { + Message msg = mHandler.obtainMessage(AdbDebuggingHandler.MESSAGE_ADB_PERSIST_KEY_STORE); + 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) { @@ -501,4 +658,231 @@ public class AdbDebuggingManager { dump.end(token); } + + /** + * Handles adb keys for which the user has granted the 'always allow' option. This class ensures + * these grants are revoked after a period of inactivity as specified in the + * ADB_ALLOWED_CONNECTION_TIME setting. + */ + class AdbKeyStore { + private Map<String, Long> mKeyMap; + private File mKeyFile; + private AtomicFile mAtomicKeyFile; + // 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 String XML_TAG_ADB_KEY = "adbKey"; + private static final String XML_ATTRIBUTE_KEY = "key"; + private static final String XML_ATTRIBUTE_LAST_CONNECTION = "lastConnection"; + + /** + * Value returned by {@code getLastConnectionTime} when there is no previously saved + * connection time for the specified key. + */ + public static final long NO_PREVIOUS_CONNECTION = 0; + + /** + * Constructor that uses the default location for the persistent adb key store. + */ + AdbKeyStore() { + initKeyFile(); + mKeyMap = getKeyMapFromFile(); + } + + /** + * Constructor that uses the specified file as the location for the persistent adb key + * store. + */ + AdbKeyStore(File keyFile) { + mKeyFile = keyFile; + initKeyFile(); + mKeyMap = getKeyMapFromFile(); + } + + /** + * Initializes the key file that will be used to persist the adb grants. + */ + private void initKeyFile() { + if (mKeyFile == null) { + mKeyFile = getAdbFile(ADB_TEMP_KEYS_FILE); + } + // getAdbFile can return null if the adb file cannot be obtained + if (mKeyFile != null) { + mAtomicKeyFile = new AtomicFile(mKeyFile); + } + } + + /** + * Returns the key map with the keys and last connection times from the key file. + */ + private Map<String, Long> getKeyMapFromFile() { + 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. + if (mAtomicKeyFile == null) { + initKeyFile(); + if (mAtomicKeyFile == null) { + Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading"); + return keyMap; + } + } + if (!mAtomicKeyFile.exists()) { + return keyMap; + } + try (FileInputStream keyStream = mAtomicKeyFile.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(keyStream, StandardCharsets.UTF_8.name()); + XmlUtils.beginDocument(parser, XML_TAG_ADB_KEY); + while (parser.next() != XmlPullParser.END_DOCUMENT) { + String tagName = parser.getName(); + if (tagName == null) { + break; + } else if (!tagName.equals(XML_TAG_ADB_KEY)) { + XmlUtils.skipCurrentTag(parser); + continue; + } + String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY); + long connectionTime; + try { + connectionTime = Long.valueOf( + parser.getAttributeValue(null, XML_ATTRIBUTE_LAST_CONNECTION)); + } catch (NumberFormatException e) { + Slog.e(TAG, + "Caught a NumberFormatException parsing the last connection time: " + + e); + XmlUtils.skipCurrentTag(parser); + continue; + } + keyMap.put(key, connectionTime); + } + } catch (IOException | XmlPullParserException e) { + Slog.e(TAG, "Caught an exception parsing the XML key file: ", e); + } + return keyMap; + } + + /** + * 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 + // are deleted as well. + if (mKeyMap.size() == 0) { + deleteKeyStore(); + return; + } + if (mAtomicKeyFile == null) { + initKeyFile(); + if (mAtomicKeyFile == null) { + Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for writing"); + return; + } + } + FileOutputStream keyStream = null; + try { + XmlSerializer serializer = new FastXmlSerializer(); + keyStream = mAtomicKeyFile.startWrite(); + serializer.setOutput(keyStream, StandardCharsets.UTF_8.name()); + serializer.startDocument(null, true); + long allowedTime = getAllowedConnectionTime(); + long systemTime = System.currentTimeMillis(); + Iterator keyMapIterator = mKeyMap.entrySet().iterator(); + while (keyMapIterator.hasNext()) { + Map.Entry<String, Long> keyEntry = (Map.Entry) keyMapIterator.next(); + long connectionTime = keyEntry.getValue(); + if (systemTime < (connectionTime + allowedTime)) { + serializer.startTag(null, XML_TAG_ADB_KEY); + serializer.attribute(null, XML_ATTRIBUTE_KEY, keyEntry.getKey()); + serializer.attribute(null, XML_ATTRIBUTE_LAST_CONNECTION, + String.valueOf(keyEntry.getValue())); + serializer.endTag(null, XML_TAG_ADB_KEY); + } else { + keyMapIterator.remove(); + } + } + serializer.endDocument(); + mAtomicKeyFile.finishWrite(keyStream); + } catch (IOException e) { + Slog.e(TAG, "Caught an exception writing the key map: ", e); + mAtomicKeyFile.failWrite(keyStream); + } + } + + /** + * Removes all of the entries in the key map and deletes the key file. + */ + public void deleteKeyStore() { + mKeyMap.clear(); + if (mAtomicKeyFile == null) { + return; + } + mAtomicKeyFile.delete(); + } + + /** + * Returns the time of the last connection from the specified key, or {@code + * NO_PREVIOUS_CONNECTION} if the specified key does not have an active adb grant. + */ + public long getLastConnectionTime(String key) { + return mKeyMap.getOrDefault(key, NO_PREVIOUS_CONNECTION); + } + + /** + * 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(); + } + + /** + * 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 boolean isKeyAuthorized(String key) { + 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 { + // since this key is no longer auhorized remove it from the Map + removeKey(key); + return false; + } + } + + /** + * Returns the connection time within which a connection from an allowed key is + * automatically allowed without user interaction. + */ + public long getAllowedConnectionTime() { + return Settings.Global.getLong(mContext.getContentResolver(), + Settings.Global.ADB_ALLOWED_CONNECTION_TIME, + Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME); + } + + /** + * Removes the specified key from the key store. + */ + public void removeKey(String key) { + if (!mKeyMap.containsKey(key)) { + return; + } + mKeyMap.remove(key); + sendPersistKeyStoreMessage(); + } + } } diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 1b5ba263e6dd..dc31c0f7ebbb 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -156,6 +156,7 @@ </activity> <activity android:name="com.android.server.accounts.AccountAuthenticatorDummyActivity" /> + <activity android:name="com.android.server.adb.AdbDebuggingManagerTestActivity" /> <activity-alias android:name="a.ShortcutEnabled" android:targetActivity="com.android.server.pm.ShortcutTestActivity" diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java new file mode 100644 index 000000000000..65af677182b4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.adb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import com.android.server.FgThread; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public final class AdbDebuggingManagerTest { + + private static final String TAG = "AdbDebuggingManagerTest"; + + // This component is passed to the AdbDebuggingManager to act as the activity that can confirm + // unknown adb keys. An overlay package was first attempted to override the + // config_customAdbPublicKeyConfirmationComponent config, but the value from that package was + // not being read. + private static final String ADB_CONFIRM_COMPONENT = + "com.android.frameworks.servicestests/" + + "com.android.server.adb.AdbDebuggingManagerTestActivity"; + + // The base64 encoding of the values 'test key 1' and 'test key 2'. + private static final String TEST_KEY_1 = "dGVzdCBrZXkgMQo="; + private static final String TEST_KEY_2 = "dGVzdCBrZXkgMgo="; + + // This response is received from the AdbDebuggingHandler when the key is allowed to connect + private static final String RESPONSE_KEY_ALLOWED = "OK"; + // This response is received from the AdbDebuggingHandler when the key is not allowed to connect + private static final String RESPONSE_KEY_DENIED = "NO"; + + // wait up to 5 seconds for any blocking queries + private static final long TIMEOUT = 5000; + private static final TimeUnit TIMEOUT_TIME_UNIT = TimeUnit.MILLISECONDS; + + private Context mContext; + private AdbDebuggingManager mManager; + private AdbDebuggingManager.AdbDebuggingThread mThread; + private AdbDebuggingManager.AdbDebuggingHandler mHandler; + private AdbDebuggingManager.AdbKeyStore mKeyStore; + private BlockingQueue<TestResult> mBlockingQueue; + private long mOriginalAllowedConnectionTime; + private File mKeyFile; + + @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(); + } + mThread = new AdbDebuggingThreadTest(); + mKeyStore = mManager.new AdbKeyStore(mKeyFile); + mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore); + mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime(); + mBlockingQueue = new ArrayBlockingQueue<>(1); + + } + + @After + public void tearDown() throws Exception { + mKeyStore.deleteKeyStore(); + setAllowedConnectionTime(mOriginalAllowedConnectionTime); + } + + /** + * Sets the allowed connection time within which a subsequent connection from a key for which + * the user selected the 'Always allow' option will be allowed without user interaction. + */ + private void setAllowedConnectionTime(long connectionTime) { + Settings.Global.putLong(mContext.getContentResolver(), + Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime); + }; + + @Test + public void testAllowNewKeyOnce() throws Exception { + // Verifies the behavior when a new key first attempts to connect to a device. During the + // first connection the ADB confirmation activity should be launched to prompt the user to + // allow the connection with an option to always allow connections from this key. + + // Verify if the user allows the key but does not select the option to 'always + // allow' that the connection is allowed but the key is not stored. + runAdbTest(TEST_KEY_1, true, false, false); + } + + @Test + public void testDenyNewKey() throws Exception { + // Verifies if the user does not allow the key then the connection is not allowed and the + // key is not stored. + runAdbTest(TEST_KEY_1, false, false, false); + } + + @Test + public void testDisconnectAlwaysAllowKey() throws Exception { + // When a key is disconnected from a device ADB should send a disconnect message; this + // message should trigger an update of the last connection time for the currently connected + // key. + + // Allow a connection from a new key with the 'Always allow' option selected. + runAdbTest(TEST_KEY_1, true, true, false); + + // Get the last connection time for the currently connected key to verify that it is updated + // after the disconnect. + long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); + + // Sleep for a small amount of time to ensure a difference can be observed in the last + // connection time after a disconnect. + Thread.sleep(10); + + // Send the disconnect message for the currently connected key to trigger an update of the + // last connection time. + 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"); + } + } + + @Test + public void testDisconnectAllowedOnceKey() throws Exception { + // When a key is disconnected ADB should send a disconnect message; this message should + // essentially result in a noop for keys that the user only allows once since the last + // connection time is not maintained for these keys. + + // Allow a connection from a new key with the 'Always allow' option set to false + runAdbTest(TEST_KEY_1, true, false, false); + + // Send the disconnect message for the currently connected key. + mHandler.obtainMessage( + AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget(); + + // Verify that the disconnected key is not automatically allowed on a subsequent connection. + runAdbTest(TEST_KEY_1, true, false, false); + } + + @Test + public void testAlwaysAllowConnectionFromKey() throws Exception { + // Verifies when the user selects the 'Always allow' option for the current key that + // subsequent connection attempts from that key are allowed. + + // Allow a connection from a new key with the 'Always allow' option selected. + runAdbTest(TEST_KEY_1, true, true, false); + + // 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); + + // Verify that a different key is not automatically allowed. + runAdbTest(TEST_KEY_2, false, false, false); + } + + @Test + public void testOriginalAlwaysAllowBehavior() throws Exception { + // If the Settings.Global.ADB_ALLOWED_CONNECTION_TIME setting is set to 0 then the original + // behavior of 'Always allow' should be restored. + + // Accept the test key with the 'Always allow' option selected. + runAdbTest(TEST_KEY_1, true, true, false); + + // Set the connection time to 0 to restore the original behavior. + setAllowedConnectionTime(0); + + // Set the last connection time to the test key to a very small value to ensure it would + // fail the new test but would be allowed with the original behavior. + mKeyStore.setLastConnectionTime(TEST_KEY_1, 1); + + // Run the test with the key and verify that the connection is automatically allowed. + runAdbTest(TEST_KEY_1, true, true, true); + } + + @Test + public void testLastConnectionTimeUpdatedByScheduledJob() throws Exception { + // If a development device is left connected to a system beyond the allowed connection time + // a reboot of the device while connected could make it appear as though the last connection + // time is beyond the allowed window. A scheduled job runs daily while a key is connected + // to update the last connection time to the current time; this ensures if the device is + // rebooted while connected to a system the last connection time should be within 24 hours. + + // Allow the key to connect with the 'Always allow' option selected + runAdbTest(TEST_KEY_1, true, true, false); + + // Get the current last connection time for comparison after the scheduled job is run + long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); + + // Sleep a small amount of time to ensure that the updated connection time changes + Thread.sleep(10); + + // Send a message to the handler to update the last connection time for the active key + 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"); + } + } + + @Test + public void testKeystorePersisted() throws Exception { + // After any updates are made to the key store a message should be sent to persist the + // key store. This test verifies that a key that is always allowed is persisted in the key + // store along with its last connection time. + + // Allow the key to connect with the 'Always allow' option selected + runAdbTest(TEST_KEY_1, true, true, false); + + // Send a message to the handler to persist the updated keystore. + 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( + "The key with the 'Always allow' option selected was not persisted in the keystore", + mManager.new AdbKeyStore(mKeyFile).isKeyAuthorized(TEST_KEY_1))); + + // Get the current last connection time to ensure it is updated in the persisted keystore. + long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); + + // Sleep a small amount of time to ensure the last connection time is updated. + Thread.sleep(10); + + // Send a message to the handler to update the last connection time for the active key. + mHandler.obtainMessage( + AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_UPDATE_KEY_CONNECTION_TIME) + .sendToTarget(); + + // 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"); + } + } + + @Test + public void testAdbClearRemovesActiveKey() throws Exception { + // If the user selects the option to 'Revoke USB debugging authorizations' while an 'Always + // allow' key is connected that key should be deleted as well. + + // Allow the key to connect with the 'Always allow' option selected + runAdbTest(TEST_KEY_1, true, true, false); + + // Send a message to the handler to clear the adb authorizations. + mHandler.obtainMessage( + AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CLEAR).sendToTarget(); + + // Send a message to disconnect the currently connected key + mHandler.obtainMessage( + AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DISCONNECT).sendToTarget(); + + // 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"); + } + } + + @Test + public void testAdbGrantRevokedIfLastConnectionBeyondAllowedTime() throws Exception { + // If the user selects the 'Always allow' option then subsequent connections from the key + // will be allowed as long as the connection is within the allowed window. Once the last + // connection time is beyond this window the user should be prompted to allow the key again. + + // Allow the key to connect with the 'Always allow' option selected + runAdbTest(TEST_KEY_1, true, true, false); + + // Set the allowed window to a small value to ensure the time is beyond the allowed window. + setAllowedConnectionTime(1); + + // Sleep for a small amount of time to exceed the allowed window. + Thread.sleep(10); + + // A new connection from this key should prompt the user again. + runAdbTest(TEST_KEY_1, true, true, false); + } + + @Test + public void testLastConnectionTimeCannotBeSetBack() throws Exception { + // When a device is first booted there is a possibility that the system time will be set to + // the build time of the system image. If a device is connected to a system during a reboot + // this could cause the connection time to be set in the past; if the device time is not + // corrected before the device is disconnected then a subsequent connection with the time + // corrected would appear as though the last connection time was beyond the allowed window, + // and the user would be required to authorize the connection again. This test verifies that + // the AdbKeyStore does not update the last connection time if it is less than the + // previously written connection time. + + // Allow the key to connect with the 'Always allow' option selected + runAdbTest(TEST_KEY_1, true, true, false); + + // Get the last connection time that was written to the key store. + long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1); + + // Attempt to set the last connection time to 1970 + mKeyStore.setLastConnectionTime(TEST_KEY_1, 0); + assertEquals( + "The last connection time in the adb key store should not be set to a value less " + + "than the previous connection time", + lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); + + // Attempt to set the last connection time just beyond the allowed window. + mKeyStore.setLastConnectionTime(TEST_KEY_1, + Math.max(0, lastConnectionTime - (mKeyStore.getAllowedConnectionTime() + 1))); + assertEquals( + "The last connection time in the adb key store should not be set to a value less " + + "than the previous connection time", + lastConnectionTime, mKeyStore.getLastConnectionTime(TEST_KEY_1)); + } + + /** + * Runs an adb test with the provided configuration. + * + * @param key The base64 encoding of the key to be used during the test. + * @param allowKey boolean indicating whether the key should be allowed to connect. + * @param alwaysAllow boolean indicating whether the 'Always allow' option should be selected. + * @param autoAllowExpected boolean indicating whether the key is expected to be automatically + * allowed without user interaction. + */ + private void runAdbTest(String key, boolean allowKey, boolean alwaysAllow, + boolean autoAllowExpected) throws Exception { + // if the key should not be automatically allowed then set up the activity + if (!autoAllowExpected) { + new AdbDebuggingManagerTestActivity.Configurator() + .setExpectedKey(key) + .setAllowKey(allowKey) + .setAlwaysAllow(alwaysAllow) + .setHandler(mHandler) + .setBlockingQueue(mBlockingQueue); + } + // send the message indicating a new key is attempting to connect + mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONFIRM, + key).sendToTarget(); + // if the key should not be automatically allowed then the ADB public key confirmation + // activity should be launched + if (!autoAllowExpected) { + TestResult activityResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT); + assertNotNull( + "The ADB public key confirmation activity did not complete within the timeout" + + " period", activityResult); + assertEquals("The ADB public key activity failed with result: " + activityResult, + TestResult.RESULT_ACTIVITY_LAUNCHED, activityResult.mReturnCode); + } + // If the activity was launched it should send a response back to the manager that would + // trigger a response to the thread, or if the key is a known valid key then a response + // should be sent back without requiring interaction with the activity. + TestResult threadResult = mBlockingQueue.poll(TIMEOUT, TIMEOUT_TIME_UNIT); + assertNotNull("A response was not sent to the thread within the timeout period", + threadResult); + // verify that the result is an expected message from the thread + assertEquals("An unexpected result was received: " + threadResult, + TestResult.RESULT_RESPONSE_RECEIVED, threadResult.mReturnCode); + assertEquals("The manager did not send the proper response for allowKey = " + allowKey, + allowKey ? RESPONSE_KEY_ALLOWED : RESPONSE_KEY_DENIED, threadResult.mMessage); + // if the key is not allowed or not always allowed verify it is not in the key store + if (!allowKey || !alwaysAllow) { + assertFalse( + "The key should not be allowed automatically on subsequent connection attempts", + mKeyStore.isKeyAuthorized(key)); + } + } + + /** + * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager + * indicating whether the key should be allowed to connect. + */ + class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread { + AdbDebuggingThreadTest() { + mManager.super(); + } + + @Override + public void sendResponse(String msg) { + TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg); + try { + mBlockingQueue.put(result); + } catch (InterruptedException e) { + Log.e(TAG, + "Caught an InterruptedException putting the result in the queue: " + result, + e); + } + } + } + + /** + * Contains the result for the current portion of the test along with any corresponding + * messages. + */ + public static class TestResult { + public int mReturnCode; + public String mMessage; + + public static final int RESULT_ACTIVITY_LAUNCHED = 1; + public static final int RESULT_UNEXPECTED_KEY = 2; + public static final int RESULT_RESPONSE_RECEIVED = 3; + + public TestResult(int returnCode) { + this(returnCode, null); + } + + public TestResult(int returnCode, String message) { + mReturnCode = returnCode; + mMessage = message; + } + + @Override + public String toString() { + return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}"; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTestActivity.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTestActivity.java new file mode 100644 index 000000000000..1a9c180ffa98 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTestActivity.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.adb; + +import static com.android.server.adb.AdbDebuggingManagerTest.TestResult; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.util.Log; + +import java.util.concurrent.BlockingQueue; + +/** + * Helper Activity used to test the AdbDebuggingManager's prompt to allow an adb key. + */ +public class AdbDebuggingManagerTestActivity extends Activity { + + private static final String TAG = "AdbDebuggingManagerTestActivity"; + + /* + * Static values that must be set before each test to modify the behavior of the Activity. + */ + private static AdbDebuggingManager.AdbDebuggingHandler sHandler; + private static boolean sAllowKey; + private static boolean sAlwaysAllow; + private static String sExpectedKey; + private static BlockingQueue sBlockingQueue; + + /** + * Receives the Intent sent from the AdbDebuggingManager and sends the preconfigured response. + */ + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + Intent intent = getIntent(); + String key = intent.getStringExtra("key"); + if (!key.equals(sExpectedKey)) { + TestResult result = new TestResult(TestResult.RESULT_UNEXPECTED_KEY, key); + postResult(result); + finish(); + return; + } + // Post the result that the activity was successfully launched as expected and a response + // is being sent to let the test method know that it should move on to waiting for the next + // expected response from the AdbDebuggingManager. + TestResult result = new TestResult( + AdbDebuggingManagerTest.TestResult.RESULT_ACTIVITY_LAUNCHED); + postResult(result); + + // Initialize the message based on the preconfigured values. If the key is accepted the + // AdbDebuggingManager expects the key to be in the obj field of the message, and if the + // user selects the 'Always allow' option the manager expects the arg1 field to be set to 1. + int messageType; + if (sAllowKey) { + messageType = AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_ALLOW; + } else { + messageType = AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_DENY; + } + Message message = sHandler.obtainMessage(messageType); + message.obj = key; + if (sAlwaysAllow) { + message.arg1 = 1; + } + finish(); + sHandler.sendMessage(message); + } + + /** + * Posts the result of the activity to the test method. + */ + private void postResult(TestResult result) { + try { + sBlockingQueue.put(result); + } catch (InterruptedException e) { + Log.e(TAG, "Caught an InterruptedException posting the result " + result, e); + } + } + + /** + * Allows test methods to specify the behavior of the Activity before it is invoked by the + * AdbDebuggingManager. + */ + public static class Configurator { + + /** + * Sets the test handler to be used by this activity to send the configured response. + */ + public Configurator setHandler(AdbDebuggingManager.AdbDebuggingHandler handler) { + sHandler = handler; + return this; + } + + /** + * Sets whether the key should be allowed for this test. + */ + public Configurator setAllowKey(boolean allow) { + sAllowKey = allow; + return this; + } + + /** + * Sets whether the 'Always allow' option should be selected for this test. + */ + public Configurator setAlwaysAllow(boolean alwaysAllow) { + sAlwaysAllow = alwaysAllow; + return this; + } + + /** + * Sets the key that should be expected from the AdbDebuggingManager for this test. + */ + public Configurator setExpectedKey(String expectedKey) { + sExpectedKey = expectedKey; + return this; + } + + /** + * Sets the BlockingQueue that should be used to post the result of the Activity back to the + * test method. + */ + public Configurator setBlockingQueue(BlockingQueue blockingQueue) { + sBlockingQueue = blockingQueue; + return this; + } + } +} |