summaryrefslogtreecommitdiff
path: root/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
blob: cffff66b64f13dc9ca5c9128b6a9bcf447c852d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.adb;

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

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

import androidx.test.InstrumentationRegistry;

import com.android.server.FgThread;

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

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

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

    private static final String TAG = "AdbDebuggingManagerTest";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        final long epsilon = 5000;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        clearKeyStore();
    }

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

        clearKeyStore();

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

    @Test
    public void testAdbKeyStore_removeKey() throws Exception {
        // Accept the test key with the 'Always allow' option selected.
        runAdbTest(TEST_KEY_1, true, true, false);
        runAdbTest(TEST_KEY_2, true, true, false);

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

        // 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));
        assertTrue("The key was not in the adb_keys file after persisting the keystore",
                isKeyInFile(TEST_KEY_2, mAdbKeyFile));

        // Now remove one of the keys and make sure the other key is still there
        mKeyStore.removeKey(TEST_KEY_1);
        assertFalse("The key was still in the adb_keys file after removing the key",
                isKeyInFile(TEST_KEY_1, mAdbKeyFile));
        assertTrue("The key was not in the adb_keys file after removing a different key",
                isKeyInFile(TEST_KEY_2, mAdbKeyFile));
    }

    @Test
    public void testIsValidMdnsServiceName() {
        // Longer than 15 characters
        assertFalse(isValidMdnsServiceName("abcd1234abcd1234"));

        // Contains invalid characters
        assertFalse(isValidMdnsServiceName("a*a"));
        assertFalse(isValidMdnsServiceName("a_a"));
        assertFalse(isValidMdnsServiceName("_a"));

        // Does not begin or end with letter or digit
        assertFalse(isValidMdnsServiceName(""));
        assertFalse(isValidMdnsServiceName("-"));
        assertFalse(isValidMdnsServiceName("-a"));
        assertFalse(isValidMdnsServiceName("-1"));
        assertFalse(isValidMdnsServiceName("a-"));
        assertFalse(isValidMdnsServiceName("1-"));

        // Contains consecutive hyphens
        assertFalse(isValidMdnsServiceName("a--a"));

        // Does not contain at least one letter
        assertFalse(isValidMdnsServiceName("1"));
        assertFalse(isValidMdnsServiceName("12"));
        assertFalse(isValidMdnsServiceName("1-2"));

        // letter not within [a-zA-Z]
        assertFalse(isValidMdnsServiceName("aés"));

        // Some valid names
        assertTrue(isValidMdnsServiceName("a"));
        assertTrue(isValidMdnsServiceName("a1"));
        assertTrue(isValidMdnsServiceName("1A"));
        assertTrue(isValidMdnsServiceName("aZ"));
        assertTrue(isValidMdnsServiceName("a-Z"));
        assertTrue(isValidMdnsServiceName("a-b-Z"));
        assertTrue(isValidMdnsServiceName("abc-def-123-456"));
    }

    @Test
    public void testPairingThread_MdnsServiceName_RFC6335() {
        assertTrue(isValidMdnsServiceName(AdbDebuggingManager.PairingThread.SERVICE_PROTOCOL));
    }

    private boolean isValidMdnsServiceName(String name) {
        // The rules for Service Names [RFC6335] state that they may be no more
        // than fifteen characters long (not counting the mandatory underscore),
        // consisting of only letters, digits, and hyphens, must begin and end
        // with a letter or digit, must not contain consecutive hyphens, and
        // must contain at least one letter.
        // No more than 15 characters long
        final int len = name.length();
        if (name.isEmpty() || len > 15) {
            return false;
        }

        boolean hasAtLeastOneLetter = false;
        boolean sawHyphen = false;
        for (int i = 0; i < len; ++i) {
            // Must contain at least one letter
            // Only contains letters, digits and hyphens
            char c = name.charAt(i);
            if (c == '-') {
                // Cannot be at beginning or end
                if (i == 0 || i == len - 1) {
                    return false;
                }
                if (sawHyphen) {
                    // Consecutive hyphen found
                    return false;
                }
                sawHyphen = true;
                continue;
            }

            sawHyphen = false;
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                hasAtLeastOneLetter = true;
                continue;
            }

            if (c >= '0' && c <= '9') {
                continue;
            }

            // Invalid character
            return false;
        }

        return hasAtLeastOneLetter;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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