summaryrefslogtreecommitdiff
path: root/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
blob: be8dec65ebe56137844107ca9500513ad8697163 (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
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
/*
 * Copyright (C) 2020 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 android.scopedstorage.cts;

import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA1;
import static android.scopedstorage.cts.lib.TestUtils.adoptShellPermissionIdentity;
import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
import static android.scopedstorage.cts.lib.TestUtils.canOpen;
import static android.scopedstorage.cts.lib.TestUtils.canReadAndWriteAs;
import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
import static android.scopedstorage.cts.lib.TestUtils.dropShellPermissionIdentity;
import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
import static android.scopedstorage.cts.lib.TestUtils.getAndroidDir;
import static android.scopedstorage.cts.lib.TestUtils.getAndroidMediaDir;
import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
import static android.scopedstorage.cts.lib.TestUtils.getDefaultTopLevelDirs;
import static android.scopedstorage.cts.lib.TestUtils.getDownloadDir;
import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
import static android.scopedstorage.cts.lib.TestUtils.getExternalMediaDir;
import static android.scopedstorage.cts.lib.TestUtils.getExternalStorageDir;
import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
import static android.scopedstorage.cts.lib.TestUtils.getFileUri;
import static android.scopedstorage.cts.lib.TestUtils.getMoviesDir;
import static android.scopedstorage.cts.lib.TestUtils.getMusicDir;
import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
import static android.scopedstorage.cts.lib.TestUtils.pollForManageExternalStorageAllowed;
import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories;
import static android.system.OsConstants.F_OK;
import static android.system.OsConstants.R_OK;
import static android.system.OsConstants.W_OK;

import static androidx.test.InstrumentationRegistry.getContext;

import static com.google.common.truth.Truth.assertThat;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;

import android.Manifest;
import android.app.WallpaperManager;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.AppModeInstant;
import android.provider.MediaStore;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;

import androidx.test.runner.AndroidJUnit4;

import com.android.cts.install.lib.TestApp;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Runs the scoped storage tests on primary external storage.
 *
 * <p>These tests are also run on a public volume by {@link PublicVolumeTest}.
 */
@RunWith(AndroidJUnit4.class)
public class ScopedStorageTest {
    static final String TAG = "ScopedStorageTest";
    static final String THIS_PACKAGE_NAME = getContext().getPackageName();
    static final int USER_SYSTEM = 0;

    /**
     * To help avoid flaky tests, give ourselves a unique nonce to be used for
     * all filesystem paths, so that we don't risk conflicting with previous
     * test runs.
     */
    static final String NONCE = String.valueOf(System.nanoTime());

    static final String TEST_DIRECTORY_NAME = "ScopedStorageTestDirectory" + NONCE;

    static final String AUDIO_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".mp3";
    static final String IMAGE_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".jpg";
    static final String NONMEDIA_FILE_NAME = "ScopedStorageTest_file_" + NONCE + ".pdf";

    // The following apps are installed before the tests are run via a target_preparer.
    // See test config for details.
    // An app with READ_EXTERNAL_STORAGE permission
    private static final TestApp APP_A_HAS_RES = new TestApp("TestAppA",
            "android.scopedstorage.cts.testapp.A.withres", 1, false,
            "CtsScopedStorageTestAppA.apk");
    // An app with no permissions
    private static final TestApp APP_B_NO_PERMS = new TestApp("TestAppB",
            "android.scopedstorage.cts.testapp.B.noperms", 1, false,
            "CtsScopedStorageTestAppB.apk");
    // A legacy targeting app with RES and WES permissions
    private static final TestApp APP_D_LEGACY_HAS_RW = new TestApp("TestAppDLegacy",
            "android.scopedstorage.cts.testapp.D", 1, false, "CtsScopedStorageTestAppCLegacy.apk");

    @Before
    public void setup() throws Exception {
        if (!getContext().getPackageManager().isInstantApp()) {
            pollForExternalStorageState();
            getExternalFilesDir().mkdirs();
        }
    }

    /**
     * This method needs to be called once before running the whole test.
     */
    @Test
    public void setupExternalStorage() {
        setupDefaultDirectories();
    }

    /**
     * Test that Installer packages can access app's private directories in Android/obb
     */
    @Test
    public void testCheckInstallerAppAccessToObbDirs() throws Exception {
        File[] obbDirs = getContext().getObbDirs();
        for (File obbDir : obbDirs) {
            final File otherAppExternalObbDir = new File(obbDir.getPath().replace(
                    THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
            final File file = new File(otherAppExternalObbDir, NONMEDIA_FILE_NAME);
            try {
                assertThat(file.exists()).isFalse();

                assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
                assertFileAccess_readWrite(file);

                assertThat(file.delete()).isTrue();
                assertThat(file.exists()).isFalse();
                assertThat(file.createNewFile()).isTrue();
                assertThat(file.exists()).isTrue();
            } finally {
                deleteFileAsNoThrow(APP_B_NO_PERMS, file.getAbsolutePath());
            }
        }
    }

    @Test
    public void testManageExternalStorageCanCreateFilesAnywhere() throws Exception {
        pollForManageExternalStorageAllowed();

        final File topLevelPdf = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
        final File musicFileInMovies = new File(getMoviesDir(), AUDIO_FILE_NAME);
        final File imageFileInDcim = new File(getDcimDir(), IMAGE_FILE_NAME);

        // Nothing special about this, anyone can create an image file in DCIM
        assertCanCreateFile(imageFileInDcim);
        // This is where we see the special powers of MANAGE_EXTERNAL_STORAGE, because it can
        // create a top level file
        assertCanCreateFile(topLevelPdf);
        // It can even create a music file in Pictures
        assertCanCreateFile(musicFileInMovies);
    }

    @Test
    public void testManageExternalStorageCantReadWriteOtherAppExternalDir() throws Exception {
        pollForManageExternalStorageAllowed();

        // Let app A create a file in its data dir
        final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
                THIS_PACKAGE_NAME, APP_A_HAS_RES.getPackageName()));
        final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
                NONMEDIA_FILE_NAME);
        assertCreateFilesAs(APP_A_HAS_RES, otherAppExternalDataFile);

        // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
        // file manager app doesn't have access to other app's external files directory
        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
        assertThat(otherAppExternalDataFile.delete()).isFalse();

        assertThat(deleteFileAs(APP_A_HAS_RES, otherAppExternalDataFile.getPath())).isTrue();

        assertThrows(IOException.class,
                () -> {
                    otherAppExternalDataFile.createNewFile();
                });
    }

    @Test
    public void testManageExternalStorageCanDeleteOtherAppsContents() throws Exception {
        pollForManageExternalStorageAllowed();

        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File otherAppImage = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
        try {
            // Create all of the files as another app
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppMusic.getPath())).isTrue();

            assertThat(otherAppPdf.delete()).isTrue();
            assertThat(otherAppPdf.exists()).isFalse();

            assertThat(otherAppImage.delete()).isTrue();
            assertThat(otherAppImage.exists()).isFalse();

            assertThat(otherAppMusic.delete()).isTrue();
            assertThat(otherAppMusic.exists()).isFalse();
        } finally {
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppMusic.getAbsolutePath());
        }
    }

    @Test
    public void testAccess_file() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);

        final File downloadDir = getDownloadDir();
        final File otherAppPdf = new File(downloadDir, "other-" + NONMEDIA_FILE_NAME);
        final File shellPdfAtRoot = new File(getExternalStorageDir(),
                "shell-" + NONMEDIA_FILE_NAME);
        final File otherAppImage = new File(getDcimDir(), "other-" + IMAGE_FILE_NAME);
        final File myAppPdf = new File(downloadDir, "my-" + NONMEDIA_FILE_NAME);
        final File doesntExistPdf = new File(downloadDir, "nada-" + NONMEDIA_FILE_NAME);

        try {
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();

            // We can read our image and pdf files.
            assertThat(myAppPdf.createNewFile()).isTrue();
            assertFileAccess_readWrite(myAppPdf);

            // We can read the other app's image file because we hold R_E_S, but we can
            // check only exists for the pdf files.
            assertFileAccess_readOnly(otherAppImage);
            assertFileAccess_existsOnly(otherAppPdf);
            assertAccess(doesntExistPdf, false, false, false);

            // We can check only exists for another app's files on root.
            createFileAsLegacyApp(shellPdfAtRoot);
            MediaStore.scanFile(getContentResolver(), shellPdfAtRoot);
            assertFileAccess_existsOnly(shellPdfAtRoot);
        } finally {
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
            deleteAsLegacyApp(shellPdfAtRoot);
            MediaStore.scanFile(getContentResolver(), shellPdfAtRoot);
            myAppPdf.delete();
        }
    }

    @Test
    public void testAccess_directory() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
        File topLevelDir = new File(getExternalStorageDir(), "Test");
        try {
            // Let app B create a file in its data dir
            final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
                    THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
            final File otherAppExternalDataSubDir = new File(otherAppExternalDataDir, "subdir");
            final File otherAppExternalDataFile = new File(otherAppExternalDataSubDir, "abc.jpg");
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppExternalDataFile.getAbsolutePath()))
                    .isTrue();

            // We cannot read or write the file, but app B can.
            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                    otherAppExternalDataFile.getAbsolutePath())).isTrue();
            assertCannotReadOrWrite(otherAppExternalDataFile);

            // We cannot read or write the dir, but app B can.
            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                    otherAppExternalDataDir.getAbsolutePath())).isTrue();
            assertCannotReadOrWrite(otherAppExternalDataDir);

            // We cannot read or write the sub dir, but app B can.
            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                    otherAppExternalDataSubDir.getAbsolutePath())).isTrue();
            assertCannotReadOrWrite(otherAppExternalDataSubDir);

            // We can read and write our own app dir, but app B cannot.
            assertThat(canReadAndWriteAs(APP_B_NO_PERMS,
                    getExternalFilesDir().getAbsolutePath())).isFalse();
            assertCanAccessMyAppFile(getExternalFilesDir());

            assertDirectoryAccess(getDcimDir(), /* exists */ true, /* canWrite */ true);
            assertDirectoryAccess(getExternalStorageDir(),true, false);
            assertDirectoryAccess(new File(getExternalStorageDir(), "Android"), true, false);
            assertDirectoryAccess(new File(getExternalStorageDir(), "doesnt/exist"), false, false);

            createDirectoryAsLegacyApp(topLevelDir);
            assertDirectoryAccess(topLevelDir, true, false);

            assertCannotReadOrWrite(new File("/storage/emulated"));
        } finally {
            deleteAsLegacyApp(topLevelDir);
        }
    }

    @Test
    public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
        pollForManageExternalStorageAllowed();

        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File pdf = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
        final File pdfInObviouslyWrongPlace = new File(getPicturesDir(), NONMEDIA_FILE_NAME);
        final File topLevelPdf = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
        final File musicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
        try {
            // Have another app create a PDF
            assertThat(createFileAs(APP_B_NO_PERMS, otherAppPdf.getPath())).isTrue();
            assertThat(otherAppPdf.exists()).isTrue();


            // Write some data to the file
            try (final FileOutputStream fos = new FileOutputStream(otherAppPdf)) {
                fos.write(BYTES_DATA1);
            }
            assertFileContent(otherAppPdf, BYTES_DATA1);

            // Assert we can rename the file and ensure the file has the same content
            assertCanRenameFile(otherAppPdf, pdf, /* checkDatabase */ false);
            assertFileContent(pdf, BYTES_DATA1);
            // We can even move it to the top level directory
            assertCanRenameFile(pdf, topLevelPdf, /* checkDatabase */ false);
            assertFileContent(topLevelPdf, BYTES_DATA1);
            // And even rename to a place where PDFs don't belong, because we're an omnipotent
            // external storage manager
            assertCanRenameFile(topLevelPdf, pdfInObviouslyWrongPlace, /* checkDatabase */ false);
            assertFileContent(pdfInObviouslyWrongPlace, BYTES_DATA1);

            // And we can even convert it into a music file, because why not?
            assertCanRenameFile(pdfInObviouslyWrongPlace, musicFile, /* checkDatabase */ false);
            assertFileContent(musicFile, BYTES_DATA1);
        } finally {
            pdf.delete();
            pdfInObviouslyWrongPlace.delete();
            topLevelPdf.delete();
            musicFile.delete();
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppPdf.getAbsolutePath());
        }
    }

    @Test
    public void testManageExternalStorageCannotRenameAndroid() throws Exception {
        pollForManageExternalStorageAllowed();

        final File androidDir = getAndroidDir();
        final File fooDir = new File(getAndroidDir().getAbsolutePath() + "foo");
        assertThat(androidDir.renameTo(fooDir)).isFalse();
    }

    @Test
    public void testManageExternalStorageReaddir() throws Exception {
        pollForManageExternalStorageAllowed();

        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
        final File otherTopLevelFile = new File(getExternalStorageDir(),
                "other" + NONMEDIA_FILE_NAME);
        try {
            assertCreateFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
            createFileAsLegacyApp(otherTopLevelFile);
            MediaStore.scanFile(getContentResolver(), otherTopLevelFile);

            // We can list other apps' files
            assertDirectoryContains(otherAppPdf.getParentFile(), otherAppPdf);
            assertDirectoryContains(otherAppImg.getParentFile(), otherAppImg);
            assertDirectoryContains(otherAppMusic.getParentFile(), otherAppMusic);
            // We can list top level files
            assertDirectoryContains(getExternalStorageDir(), otherTopLevelFile);

            // We can also list all top level directories
            assertDirectoryContains(getExternalStorageDir(), getDefaultTopLevelDirs());
        } finally {
            deleteAsLegacyApp(otherTopLevelFile);
            MediaStore.scanFile(getContentResolver(), otherTopLevelFile);
            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
        }
    }

    @Test
    public void testManageExternalStorageQueryOtherAppsFile() throws Exception {
        pollForManageExternalStorageAllowed();

        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
        try {
            // Apps can't query other app's pending file, hence create file and publish it.
            assertCreatePublishedFilesAs(
                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);

            assertCanQueryAndOpenFile(otherAppPdf, "rw");
            assertCanQueryAndOpenFile(otherAppImg, "rw");
            assertCanQueryAndOpenFile(otherAppMusic, "rw");
            assertCanQueryAndOpenFile(otherHiddenFile, "rw");
        } finally {
            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
        }
    }

    /*
     * b/174211425: Test that for apps bypassing database operations we mark the nomedia directory
     * as dirty for create/rename/delete.
     */
    @Test
    public void testManageExternalStorageDoesntSkipScanningDirtyNomediaDir() throws Exception {
        pollForManageExternalStorageAllowed();

        final File nomediaDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
        final File nomediaFile = new File(nomediaDir, ".nomedia");
        final File mediaFile = new File(nomediaDir, IMAGE_FILE_NAME);
        final File renamedMediaFile = new File(nomediaDir, "Renamed_" + IMAGE_FILE_NAME);
        try {
            if (!nomediaDir.exists()) {
                assertTrue(nomediaDir.mkdirs());
            }
            assertThat(nomediaFile.createNewFile()).isTrue();
            MediaStore.scanFile(getContentResolver(), nomediaDir);

            assertThat(mediaFile.createNewFile()).isTrue();
            MediaStore.scanFile(getContentResolver(), nomediaDir);
            assertThat(getFileRowIdFromDatabase(mediaFile)).isNotEqualTo(-1);

            assertThat(mediaFile.renameTo(renamedMediaFile)).isTrue();
            MediaStore.scanFile(getContentResolver(), nomediaDir);
            assertThat(getFileRowIdFromDatabase(renamedMediaFile)).isNotEqualTo(-1);

            assertThat(renamedMediaFile.delete()).isTrue();
            MediaStore.scanFile(getContentResolver(), nomediaDir);
            assertThat(getFileRowIdFromDatabase(renamedMediaFile)).isEqualTo(-1);
        } finally {
            nomediaFile.delete();
            mediaFile.delete();
            renamedMediaFile.delete();
            nomediaDir.delete();
        }
    }

    @Test
    public void testScanDoesntSkipDirtySubtree() throws Exception {
        pollForManageExternalStorageAllowed();

        final File nomediaDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
        final File topLevelNomediaFile = new File(nomediaDir, ".nomedia");
        final File nomediaSubDir = new File(nomediaDir, "child_" + TEST_DIRECTORY_NAME);
        final File nomediaFileInSubDir = new File(nomediaSubDir, ".nomedia");
        final File mediaFile1InSubDir = new File(nomediaSubDir, "1_" + IMAGE_FILE_NAME);
        final File mediaFile2InSubDir = new File(nomediaSubDir, "2_" + IMAGE_FILE_NAME);
        try {
            if (!nomediaDir.exists()) {
                assertTrue(nomediaDir.mkdirs());
            }
            if (!nomediaSubDir.exists()) {
                assertTrue(nomediaSubDir.mkdirs());
            }
            assertThat(topLevelNomediaFile.createNewFile()).isTrue();
            assertThat(nomediaFileInSubDir.createNewFile()).isTrue();
            MediaStore.scanFile(getContentResolver(), nomediaDir);

            // Verify creating a new file in subdirectory sets dirty state, and scanning the top
            // level nomedia directory will not skip scanning the subdirectory.
            assertCreateFileAndScanNomediaDirDoesntNoOp(mediaFile1InSubDir, nomediaDir);

            // Verify creating a new file in subdirectory sets dirty state, and scanning the
            // subdirectory will not no-op.
            assertCreateFileAndScanNomediaDirDoesntNoOp(mediaFile2InSubDir, nomediaSubDir);
        } finally {
            nomediaFileInSubDir.delete();
            mediaFile1InSubDir.delete();
            mediaFile2InSubDir.delete();
            topLevelNomediaFile.delete();
            nomediaSubDir.delete();
            nomediaDir.delete();
            // Scan the directory to remove stale db rows.
            MediaStore.scanFile(getContentResolver(), nomediaDir);
        }
    }

    @Test
    public void testAndroidMedia() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);

        final File myMediaDir = getExternalMediaDir();
        final File otherAppMediaDir = new File(myMediaDir.getAbsolutePath()
                .replace(THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));

        // Verify that accessing other app's /sdcard/Android/media behaves exactly like DCIM for
        // image files and exactly like Downloads for documents.
        assertSharedStorageAccess(otherAppMediaDir, otherAppMediaDir, APP_B_NO_PERMS);
        assertSharedStorageAccess(getDcimDir(), getDownloadDir(), APP_B_NO_PERMS);
    }

    @Test
    public void testWallpaperApisNoPermission() throws Exception {
        WallpaperManager wallpaperManager = WallpaperManager.getInstance(getContext());
        assumeTrue("Test skipped as wallpaper is not supported.",
                wallpaperManager.isWallpaperSupported());
        assertThrows(SecurityException.class, () -> wallpaperManager.getFastDrawable());
        assertThrows(SecurityException.class, () -> wallpaperManager.peekFastDrawable());
        assertThrows(SecurityException.class,
                () -> wallpaperManager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM));
    }

    @Test
    public void testWallpaperApisReadExternalStorage() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
        WallpaperManager wallpaperManager = WallpaperManager.getInstance(getContext());
        wallpaperManager.getFastDrawable();
        wallpaperManager.peekFastDrawable();
        wallpaperManager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM);
    }

    @Test
    public void testWallpaperApisManageExternalStorageAppOp() throws Exception {
        pollForManageExternalStorageAllowed();

        WallpaperManager wallpaperManager = WallpaperManager.getInstance(getContext());
        wallpaperManager.getFastDrawable();
        wallpaperManager.peekFastDrawable();
        wallpaperManager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM);
    }

    @Test
    public void testWallpaperApisManageExternalStoragePrivileged() throws Exception {
        adoptShellPermissionIdentity(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
        try {
            WallpaperManager wallpaperManager = WallpaperManager.getInstance(getContext());
            wallpaperManager.getFastDrawable();
            wallpaperManager.peekFastDrawable();
            wallpaperManager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM);
        } finally {
            dropShellPermissionIdentity();
        }
    }

    private void assertCreateFileAndScanNomediaDirDoesntNoOp(File newFile, File scanDir)
            throws Exception {
        assertThat(newFile.createNewFile()).isTrue();
        // File is not added to database yet, but the directory is marked as dirty so that next
        // scan doesn't no-op.
        assertThat(getFileRowIdFromDatabase(newFile)).isEqualTo(-1);

        MediaStore.scanFile(getContentResolver(), scanDir);
        assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
    }

    /**
     * Verifies that files created by {@code otherApp} in shared locations {@code imageDir}
     * and {@code documentDir} follow the scoped storage rules. Requires the running app to hold
     * {@code READ_EXTERNAL_STORAGE}.
     */
    private void assertSharedStorageAccess(File imageDir, File documentDir, TestApp otherApp)
            throws Exception {
        final File otherAppImage = new File(imageDir, "abc.jpg");
        final File otherAppBinary = new File(documentDir, "abc.bin");
        try {
            assertCreateFilesAs(otherApp, otherAppImage, otherAppBinary);

            // We can read the other app's image
            assertFileAccess_readOnly(otherAppImage);
            assertFileContent(otherAppImage, new String().getBytes());

            // .. but not the binary file
            assertFileAccess_existsOnly(otherAppBinary);
            assertThrows(FileNotFoundException.class, () -> {
                assertFileContent(otherAppBinary, new String().getBytes());
            });
        } finally {
            deleteFileAsNoThrow(otherApp, otherAppImage.getAbsolutePath());
            deleteFileAsNoThrow(otherApp, otherAppBinary.getAbsolutePath());
        }
    }


    @Test
    public void testOpenOtherPendingFilesFromFuse() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
        final File otherPendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
        try {
            assertCreateFilesAs(APP_B_NO_PERMS, otherPendingFile);

            // We can read other app's pending file from FUSE via filePath
            assertCanQueryAndOpenFile(otherPendingFile, "r");

            // We can also read other app's pending file via MediaStore API
            assertNotNull(openWithMediaProvider(otherPendingFile, "r"));
        } finally {
            deleteFileAsNoThrow(APP_B_NO_PERMS, otherPendingFile.getAbsolutePath());
        }
    }

    @Test
    public void testNoIsolatedStorageCanCreateFilesAnywhere() throws Exception {
        final File topLevelPdf = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
        final File musicFileInMovies = new File(getMoviesDir(), AUDIO_FILE_NAME);
        final File imageFileInDcim = new File(getDcimDir(), IMAGE_FILE_NAME);
        // Nothing special about this, anyone can create an image file in DCIM
        assertCanCreateFile(imageFileInDcim);
        // This is where we see the special powers of MANAGE_EXTERNAL_STORAGE, because it can
        // create a top level file
        assertCanCreateFile(topLevelPdf);
        // It can even create a music file in Pictures
        assertCanCreateFile(musicFileInMovies);
    }

    @Test
    public void testNoIsolatedStorageCantReadWriteOtherAppExternalDir() throws Exception {
        // Let app A create a file in its data dir
        final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
                THIS_PACKAGE_NAME, APP_A_HAS_RES.getPackageName()));
        final File otherAppExternalDataFile = new File(otherAppExternalDataDir,
                NONMEDIA_FILE_NAME);
        assertCreateFilesAs(APP_A_HAS_RES, otherAppExternalDataFile);

        // File Manager app gets global access with MANAGE_EXTERNAL_STORAGE permission, however,
        // file manager app doesn't have access to other app's external files directory
        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ false)).isFalse();
        assertThat(canOpen(otherAppExternalDataFile, /* forWrite */ true)).isFalse();
        assertThat(otherAppExternalDataFile.delete()).isFalse();

        assertThat(deleteFileAs(APP_A_HAS_RES, otherAppExternalDataFile.getPath())).isTrue();

        assertThrows(IOException.class,
                () -> {
                    otherAppExternalDataFile.createNewFile();
                });
    }

    @Test
    public void testNoIsolatedStorageStorageReaddir() throws Exception {
        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
        final File otherTopLevelFile = new File(getExternalStorageDir(),
                "other" + NONMEDIA_FILE_NAME);
        try {
            assertCreateFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
            createFileAsLegacyApp(otherTopLevelFile);

            // We can list other apps' files
            assertDirectoryContains(otherAppPdf.getParentFile(), otherAppPdf);
            assertDirectoryContains(otherAppImg.getParentFile(), otherAppImg);
            assertDirectoryContains(otherAppMusic.getParentFile(), otherAppMusic);
            // We can list top level files
            assertDirectoryContains(getExternalStorageDir(), otherTopLevelFile);

            // We can also list all top level directories
            assertDirectoryContains(getExternalStorageDir(), getDefaultTopLevelDirs());
        } finally {
            deleteAsLegacyApp(otherTopLevelFile);
            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf);
        }
    }

    @Test
    public void testNoIsolatedStorageQueryOtherAppsFile() throws Exception {
        final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
        final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
        final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
        final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
        try {
            // Apps can't query other app's pending file, hence create file and publish it.
            assertCreatePublishedFilesAs(
                    APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);

            assertCanQueryAndOpenFile(otherAppPdf, "rw");
            assertCanQueryAndOpenFile(otherAppImg, "rw");
            assertCanQueryAndOpenFile(otherAppMusic, "rw");
            assertCanQueryAndOpenFile(otherHiddenFile, "rw");
        } finally {
            deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
        }
    }

    @Test
    public void testRenameFromShell() throws Exception {
        // This test is for shell and shell always runs as USER_SYSTEM
        assumeTrue("Test is applicable only for System User.", getCurrentUser() == USER_SYSTEM);
        final File imageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
        final File dir = new File(getMoviesDir(), TEST_DIRECTORY_NAME);
        final File renamedDir = new File(getMusicDir(), TEST_DIRECTORY_NAME);
        final File renamedImageFile = new File(dir, IMAGE_FILE_NAME);
        final File imageFileInRenamedDir = new File(renamedDir, IMAGE_FILE_NAME);
        try {
            assertTrue(imageFile.createNewFile());
            assertThat(getFileRowIdFromDatabase(imageFile)).isNotEqualTo(-1);
            if (!dir.exists()) {
                assertThat(dir.mkdir()).isTrue();
            }

            final String renameFileCommand = String.format("mv %s %s",
                    imageFile.getAbsolutePath(), renamedImageFile.getAbsolutePath());
            executeShellCommand(renameFileCommand);
            assertFalse(imageFile.exists());
            assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(-1);
            assertTrue(renamedImageFile.exists());
            assertThat(getFileRowIdFromDatabase(renamedImageFile)).isNotEqualTo(-1);

            final String renameDirectoryCommand = String.format("mv %s %s",
                    dir.getAbsolutePath(), renamedDir.getAbsolutePath());
            executeShellCommand(renameDirectoryCommand);
            assertFalse(dir.exists());
            assertFalse(renamedImageFile.exists());
            assertThat(getFileRowIdFromDatabase(renamedImageFile)).isEqualTo(-1);
            assertTrue(renamedDir.exists());
            assertTrue(imageFileInRenamedDir.exists());
            assertThat(getFileRowIdFromDatabase(imageFileInRenamedDir)).isNotEqualTo(-1);
        } finally {
            imageFile.delete();
            renamedImageFile.delete();
            imageFileInRenamedDir.delete();
            dir.delete();
            renamedDir.delete();
        }
    }

    @Test
    public void testClearPackageData() throws Exception {
        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);

        File fileToRemain = new File(getPicturesDir(), IMAGE_FILE_NAME);
        String testAppPackageName = APP_B_NO_PERMS.getPackageName();
        File fileToBeDeleted =
                new File(
                        getAndroidMediaDir(),
                        String.format("%s/%s", testAppPackageName, IMAGE_FILE_NAME));
        File nestedFileToBeDeleted =
                new File(
                        getAndroidMediaDir(),
                        String.format("%s/nesteddir/%s", testAppPackageName, IMAGE_FILE_NAME));

        try {
            createAndCheckFileAsApp(APP_B_NO_PERMS, fileToRemain);
            createAndCheckFileAsApp(APP_B_NO_PERMS, fileToBeDeleted);
            createAndCheckFileAsApp(APP_B_NO_PERMS, nestedFileToBeDeleted);

            executeShellCommand("pm clear " + testAppPackageName);

            // Wait a max of 5 seconds for the cleaning after "pm clear" command to complete.
            int i = 0;
            while(i < 10 && getFileRowIdFromDatabase(fileToBeDeleted) != -1
                && getFileRowIdFromDatabase(nestedFileToBeDeleted) != -1) {
                Thread.sleep(500);
                i++;
            }

            assertThat(getFileOwnerPackageFromDatabase(fileToRemain)).isNull();
            assertThat(getFileRowIdFromDatabase(fileToRemain)).isNotEqualTo(-1);

            assertThat(getFileOwnerPackageFromDatabase(fileToBeDeleted)).isNull();
            assertThat(getFileRowIdFromDatabase(fileToBeDeleted)).isEqualTo(-1);

            assertThat(getFileOwnerPackageFromDatabase(nestedFileToBeDeleted)).isNull();
            assertThat(getFileRowIdFromDatabase(nestedFileToBeDeleted)).isEqualTo(-1);
        } finally {
            deleteFilesAs(APP_B_NO_PERMS, fileToRemain);
            deleteFilesAs(APP_B_NO_PERMS, fileToBeDeleted);
            deleteFilesAs(APP_B_NO_PERMS, nestedFileToBeDeleted);
        }
    }

    /**
     * Tests that an instant app can't access external storage.
     */
    @Test
    @AppModeInstant
    public void testInstantAppsCantAccessExternalStorage() throws Exception {
        assumeTrue("This test requires that the test runs as an Instant app",
                getContext().getPackageManager().isInstantApp());
        assertThat(getContext().getPackageManager().isInstantApp()).isTrue();

        // Can't read ExternalStorageDir
        assertThat(getExternalStorageDir().list()).isNull();

        // Can't create a top-level direcotry
        final File topLevelDir = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME);
        assertThat(topLevelDir.mkdir()).isFalse();

        // Can't create file under root dir
        final File newTxtFile = new File(getExternalStorageDir(), NONMEDIA_FILE_NAME);
        assertThrows(IOException.class,
                () -> {
                    newTxtFile.createNewFile();
                });

        // Can't create music file under /MUSIC
        final File newMusicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
        assertThrows(IOException.class,
                () -> {
                    newMusicFile.createNewFile();
                });

        // getExternalFilesDir() is not null
        assertThat(getExternalFilesDir()).isNotNull();

        // Can't read/write app specific dir
        assertThat(getExternalFilesDir().list()).isNull();
        assertThat(getExternalFilesDir().exists()).isFalse();
    }

    private void createAndCheckFileAsApp(TestApp testApp, File newFile) throws Exception {
        assertThat(createFileAs(testApp, newFile.getPath())).isTrue();
        assertThat(getFileOwnerPackageFromDatabase(newFile))
            .isEqualTo(testApp.getPackageName());
        assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1);
    }

    private static void assertCreateFilesAs(TestApp testApp, File... files) throws Exception {
        for (File file : files) {
            assertFalse("File already exists: " + file, file.exists());
            assertTrue("Failed to create file " + file + " on behalf of "
                            + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
        }
    }

    /**
     * Makes {@code testApp} create {@code files}. Publishes {@code files} by scanning the file.
     * Pending files from FUSE are not visible to other apps via MediaStore APIs. We have to publish
     * the file or make the file non-pending to make the file visible to other apps.
     * <p>
     * Note that this method can only be used for scannable files.
     */
    private static void assertCreatePublishedFilesAs(TestApp testApp, File... files)
            throws Exception {
        for (File file : files) {
            assertTrue("Failed to create published file " + file + " on behalf of "
                    + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
            assertNotNull("Failed to scan " + file,
                    MediaStore.scanFile(getContentResolver(), file));
        }
    }

    private static void deleteFilesAs(TestApp testApp, File... files) throws Exception {
        for (File file : files) {
            deleteFileAs(testApp, file.getPath());
        }
    }

    /**
     * For possible values of {@code mode}, look at {@link android.content.ContentProvider#openFile}
     */
    private static void assertCanQueryAndOpenFile(File file, String mode) throws IOException {
        // This call performs the query
        final Uri fileUri = getFileUri(file);
        // The query succeeds iff it didn't return null
        assertThat(fileUri).isNotNull();
        // Now we assert that we can open the file through ContentResolver
        try (final ParcelFileDescriptor pfd =
                        getContentResolver().openFileDescriptor(fileUri, mode)) {
            assertThat(pfd).isNotNull();
        }
    }

    private static void assertCanCreateFile(File file) throws IOException {
        // If the file somehow managed to survive a previous run, then the test app was uninstalled
        // and MediaProvider will remove our its ownership of the file, so it's not guaranteed that
        // we can create nor delete it.
        if (!file.exists()) {
            assertThat(file.createNewFile()).isTrue();
            assertThat(file.delete()).isTrue();
        } else {
            Log.w(TAG,
                    "Couldn't assertCanCreateFile(" + file + ") because file existed prior to "
                            + "running the test!");
        }
    }

    private static void assertFileAccess_existsOnly(File file) throws Exception {
        assertThat(file.isFile()).isTrue();
        assertAccess(file, true, false, false);
    }

    private static void assertFileAccess_readOnly(File file) throws Exception {
        assertThat(file.isFile()).isTrue();
        assertAccess(file, true, true, false);
    }

    private static void assertFileAccess_readWrite(File file) throws Exception {
        assertThat(file.isFile()).isTrue();
        assertAccess(file, true, true, true);
    }

    private static void assertDirectoryAccess(File dir, boolean exists, boolean canWrite)
            throws Exception {
        // This util does not handle app data directories.
        assumeFalse(dir.getAbsolutePath().startsWith(getAndroidDir().getAbsolutePath())
                && !dir.equals(getAndroidDir()));
        assertThat(dir.isDirectory()).isEqualTo(exists);
        // For non-app data directories, exists => canRead().
        assertAccess(dir, exists, exists, exists && canWrite);
    }

    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)
            throws Exception {
        assertAccess(file, exists, canRead, canWrite, true /* checkExists */);
    }

    private static void assertCannotReadOrWrite(File file)
            throws Exception {
        // App data directories have different 'x' bits on upgrading vs new devices. Let's not
        // check 'exists', by passing checkExists=false. But assert this app cannot read or write
        // the other app's file.
        assertAccess(file, false /* value is moot */, false /* canRead */,
                false /* canWrite */, false /* checkExists */);
    }

    private static void assertCanAccessMyAppFile(File file)
            throws Exception {
        assertAccess(file, true, true /* canRead */,
                true /*canWrite */, true /* checkExists */);
    }

    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite,
            boolean checkExists) throws Exception {
        if (checkExists) {
            assertThat(file.exists()).isEqualTo(exists);
        }
        assertThat(file.canRead()).isEqualTo(canRead);
        assertThat(file.canWrite()).isEqualTo(canWrite);
        if (file.isDirectory()) {
            if (checkExists) {
                assertThat(file.canExecute()).isEqualTo(exists);
            }
        } else {
            assertThat(file.canExecute()).isFalse(); // Filesytem is mounted with MS_NOEXEC
        }

        // Test some combinations of mask.
        assertAccess(file, R_OK, canRead);
        assertAccess(file, W_OK, canWrite);
        assertAccess(file, R_OK | W_OK, canRead && canWrite);
        assertAccess(file, W_OK | F_OK, canWrite);

        if (checkExists) {
            assertAccess(file, F_OK, exists);
        }
    }

    private static void assertAccess(File file, int mask, boolean expected) throws Exception {
        if (expected) {
            assertThat(Os.access(file.getAbsolutePath(), mask)).isTrue();
        } else {
            assertThrows(ErrnoException.class, () -> { Os.access(file.getAbsolutePath(), mask); });
        }
    }

    /**
     * Creates a file at any location on storage (except external app data directory).
     * The owner of the file is not the caller app.
     */
    private void createFileAsLegacyApp(File file) throws Exception {
        // Use a legacy app to create this file, since it could be outside shared storage.
        Log.d(TAG, "Creating file " + file);
        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath())).isTrue();
    }

    /**
     * Creates a file at any location on storage (except external app data directory).
     * The owner of the file is not the caller app.
     */
    private void createDirectoryAsLegacyApp(File file) throws Exception {
        // Use a legacy app to create this file, since it could be outside shared storage.
        Log.d(TAG, "Creating directory " + file);
        // Create a tmp file in the target directory, this would also create the required
        // directory, then delete the tmp file. It would leave only new directory.
        assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
        assertThat(deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
    }

    /**
     * Deletes a file at any location on storage (except external app data directory).
     */
    private void deleteAsLegacyApp(File file) throws Exception {
        // Use a legacy app to delete this file, since it could be outside shared storage.
        Log.d(TAG, "Deleting file " + file);
        deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath());
    }

    private int getCurrentUser() throws Exception {
        String userId = executeShellCommand("am get-current-user");
        return Integer.parseInt(userId.trim());
    }
}