1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.scopedstorage.cts.device;
18 
19 import static android.app.AppOpsManager.permissionToOp;
20 import static android.os.ParcelFileDescriptor.MODE_CREATE;
21 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
22 import static android.os.SystemProperties.getBoolean;
23 import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMatch;
24 import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMismatch;
25 import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
26 import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromRawResource;
27 import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
28 import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
29 import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid;
30 import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory;
31 import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile;
32 import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameDirectory;
33 import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
34 import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains;
35 import static android.scopedstorage.cts.lib.TestUtils.assertFileContent;
36 import static android.scopedstorage.cts.lib.TestUtils.assertMountMode;
37 import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
38 import static android.scopedstorage.cts.lib.TestUtils.canOpen;
39 import static android.scopedstorage.cts.lib.TestUtils.canOpenFileAs;
40 import static android.scopedstorage.cts.lib.TestUtils.canQueryOnUri;
41 import static android.scopedstorage.cts.lib.TestUtils.checkPermission;
42 import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
43 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAs;
44 import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow;
45 import static android.scopedstorage.cts.lib.TestUtils.deleteRecursively;
46 import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProvider;
47 import static android.scopedstorage.cts.lib.TestUtils.deleteWithMediaProviderNoThrow;
48 import static android.scopedstorage.cts.lib.TestUtils.denyAppOpsToUid;
49 import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand;
50 import static android.scopedstorage.cts.lib.TestUtils.getAlarmsDir;
51 import static android.scopedstorage.cts.lib.TestUtils.getAndroidDataDir;
52 import static android.scopedstorage.cts.lib.TestUtils.getAndroidMediaDir;
53 import static android.scopedstorage.cts.lib.TestUtils.getAudiobooksDir;
54 import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
55 import static android.scopedstorage.cts.lib.TestUtils.getDcimDir;
56 import static android.scopedstorage.cts.lib.TestUtils.getDocumentsDir;
57 import static android.scopedstorage.cts.lib.TestUtils.getDownloadDir;
58 import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
59 import static android.scopedstorage.cts.lib.TestUtils.getExternalMediaDir;
60 import static android.scopedstorage.cts.lib.TestUtils.getExternalStorageDir;
61 import static android.scopedstorage.cts.lib.TestUtils.getFileMimeTypeFromDatabase;
62 import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase;
63 import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase;
64 import static android.scopedstorage.cts.lib.TestUtils.getFileSizeFromDatabase;
65 import static android.scopedstorage.cts.lib.TestUtils.getFileUri;
66 import static android.scopedstorage.cts.lib.TestUtils.getImageContentUri;
67 import static android.scopedstorage.cts.lib.TestUtils.getMoviesDir;
68 import static android.scopedstorage.cts.lib.TestUtils.getMusicDir;
69 import static android.scopedstorage.cts.lib.TestUtils.getNotificationsDir;
70 import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir;
71 import static android.scopedstorage.cts.lib.TestUtils.getPodcastsDir;
72 import static android.scopedstorage.cts.lib.TestUtils.getRecordingsDir;
73 import static android.scopedstorage.cts.lib.TestUtils.getRingtonesDir;
74 import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
75 import static android.scopedstorage.cts.lib.TestUtils.installApp;
76 import static android.scopedstorage.cts.lib.TestUtils.installAppWithStoragePermissions;
77 import static android.scopedstorage.cts.lib.TestUtils.isAppInstalled;
78 import static android.scopedstorage.cts.lib.TestUtils.listAs;
79 import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
80 import static android.scopedstorage.cts.lib.TestUtils.queryFile;
81 import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
82 import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
83 import static android.scopedstorage.cts.lib.TestUtils.queryVideoFile;
84 import static android.scopedstorage.cts.lib.TestUtils.readExifMetadataFromTestApp;
85 import static android.scopedstorage.cts.lib.TestUtils.revokePermission;
86 import static android.scopedstorage.cts.lib.TestUtils.setAppOpsModeForUid;
87 import static android.scopedstorage.cts.lib.TestUtils.setAttrAs;
88 import static android.scopedstorage.cts.lib.TestUtils.uninstallApp;
89 import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
90 import static android.scopedstorage.cts.lib.TestUtils.updateDisplayNameWithMediaProvider;
91 import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalMediaDirViaRelativePath_allowed;
92 import static android.scopedstorage.cts.lib.TestUtils.verifyInsertFromExternalPrivateDirViaRelativePath_denied;
93 import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalMediaDirViaRelativePath_allowed;
94 import static android.scopedstorage.cts.lib.TestUtils.verifyUpdateToExternalPrivateDirsViaRelativePath_denied;
95 import static android.system.OsConstants.F_OK;
96 import static android.system.OsConstants.O_APPEND;
97 import static android.system.OsConstants.O_CREAT;
98 import static android.system.OsConstants.O_EXCL;
99 import static android.system.OsConstants.O_RDWR;
100 import static android.system.OsConstants.O_TRUNC;
101 import static android.system.OsConstants.R_OK;
102 import static android.system.OsConstants.S_IRWXU;
103 import static android.system.OsConstants.W_OK;
104 
105 import static androidx.test.InstrumentationRegistry.getContext;
106 
107 import static com.google.common.truth.Truth.assertThat;
108 import static com.google.common.truth.Truth.assertWithMessage;
109 
110 import static junit.framework.Assert.assertFalse;
111 import static junit.framework.Assert.assertTrue;
112 
113 import static org.junit.Assert.assertEquals;
114 import static org.junit.Assert.assertNotNull;
115 
116 import android.Manifest;
117 import android.app.AppOpsManager;
118 import android.content.ContentResolver;
119 import android.content.ContentValues;
120 import android.content.pm.ProviderInfo;
121 import android.database.Cursor;
122 import android.net.Uri;
123 import android.os.Bundle;
124 import android.os.Environment;
125 import android.os.FileUtils;
126 import android.os.ParcelFileDescriptor;
127 import android.os.Process;
128 import android.os.storage.StorageManager;
129 import android.provider.DocumentsContract;
130 import android.provider.MediaStore;
131 import android.system.ErrnoException;
132 import android.system.Os;
133 import android.system.StructStat;
134 import android.util.Log;
135 
136 import androidx.annotation.Nullable;
137 import androidx.test.filters.SdkSuppress;
138 
139 import com.android.cts.install.lib.TestApp;
140 
141 import com.google.common.io.Files;
142 
143 import org.junit.After;
144 import org.junit.Before;
145 import org.junit.BeforeClass;
146 import org.junit.Test;
147 import org.junit.runner.RunWith;
148 import org.junit.runners.Parameterized;
149 import org.junit.runners.Parameterized.Parameter;
150 import org.junit.runners.Parameterized.Parameters;
151 
152 import java.io.File;
153 import java.io.FileDescriptor;
154 import java.io.FileNotFoundException;
155 import java.io.FileOutputStream;
156 import java.io.IOException;
157 import java.io.InputStream;
158 import java.nio.ByteBuffer;
159 import java.util.Arrays;
160 import java.util.HashMap;
161 import java.util.List;
162 
163 /**
164  * Device-side test suite to verify scoped storage business logic.
165  */
166 @RunWith(Parameterized.class)
167 public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
168     public static final String STR_DATA1 = "Just some random text";
169 
170     public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
171 
172     static final String TAG = "ScopedStorageDeviceTest";
173     static final String THIS_PACKAGE_NAME = getContext().getPackageName();
174 
175     /**
176      * To help avoid flaky tests, give ourselves a unique nonce to be used for
177      * all filesystem paths, so that we don't risk conflicting with previous
178      * test runs.
179      */
180     static final String NONCE = String.valueOf(System.nanoTime());
181 
182     static final String TEST_DIRECTORY_NAME = "ScopedStorageDeviceTestDirectory" + NONCE;
183 
184     static final String AUDIO_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".mp3";
185     static final String PLAYLIST_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".m3u";
186     static final String SUBTITLE_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".srt";
187     static final String VIDEO_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".mp4";
188     static final String IMAGE_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".jpg";
189     static final String NONMEDIA_FILE_NAME = "ScopedStorageDeviceTest_file_" + NONCE + ".pdf";
190 
191     static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
192 
193     // The following apps are installed before the tests are run via a target_preparer.
194     // See test config for details.
195     // An app with READ_EXTERNAL_STORAGE permission
196     private static final TestApp APP_A_HAS_RES = new TestApp("TestAppA",
197             "android.scopedstorage.cts.testapp.A.withres", 1, false,
198             "CtsScopedStorageTestAppA.apk");
199     // An app with no permissions
200     private static final TestApp APP_B_NO_PERMS = new TestApp("TestAppB",
201             "android.scopedstorage.cts.testapp.B.noperms", 1, false,
202             "CtsScopedStorageTestAppB.apk");
203     // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
204     private static final TestApp APP_FM = new TestApp("TestAppFileManager",
205             "android.scopedstorage.cts.testapp.filemanager", 1, false,
206             "CtsScopedStorageTestAppFileManager.apk");
207     // A legacy targeting app with RES and WES permissions
208     private static final TestApp APP_D_LEGACY_HAS_RW = new TestApp("TestAppDLegacy",
209             "android.scopedstorage.cts.testapp.D", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
210 
211     // The following apps are not installed at test startup - please install before using.
212     private static final TestApp APP_C = new TestApp("TestAppC",
213             "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppC.apk");
214     private static final TestApp APP_C_LEGACY = new TestApp("TestAppCLegacy",
215             "android.scopedstorage.cts.testapp.C", 1, false, "CtsScopedStorageTestAppCLegacy.apk");
216 
217     private static final String[] SYSTEM_GALERY_APPOPS = {
218             AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO};
219     private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
220             permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
221 
222     private static final String TRANSFORMS_DIR = ".transforms";
223     private static final String TRANSFORMS_TRANSCODE_DIR = TRANSFORMS_DIR + "/" + "transcode";
224     private static final String TRANSFORMS_SYNTHETIC_DIR = TRANSFORMS_DIR + "/" + "synthetic";
225 
226     @Parameter(0)
227     public String mVolumeName;
228 
229     /** Parameters data. */
230     @Parameters(name = "volume={0}")
data()231     public static Iterable<? extends Object> data() {
232         return ScopedStorageDeviceTest.getTestParameters();
233     }
234 
235     @BeforeClass
setupApps()236     public static void setupApps() throws Exception {
237         // File manager needs to be explicitly granted MES app op.
238         final int fmUid =
239                 getContext().getPackageManager().getPackageUid(APP_FM.getPackageName(),
240                         0);
241         allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
242 
243         // Others are installed by target preparer with runtime permissions.
244         // Verify.
245         assertThat(checkPermission(APP_A_HAS_RES,
246                 Manifest.permission.READ_EXTERNAL_STORAGE)).isTrue();
247         assertThat(checkPermission(APP_B_NO_PERMS,
248                 Manifest.permission.READ_EXTERNAL_STORAGE)).isFalse();
249         assertThat(checkPermission(APP_D_LEGACY_HAS_RW,
250                 Manifest.permission.READ_EXTERNAL_STORAGE)).isTrue();
251         assertThat(checkPermission(APP_D_LEGACY_HAS_RW,
252                 Manifest.permission.WRITE_EXTERNAL_STORAGE)).isTrue();
253     }
254 
255     @After
tearDown()256     public void tearDown() throws Exception {
257         executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
258     }
259 
260     @Before
setupExternalStorage()261     public void setupExternalStorage() {
262         super.setupExternalStorage(mVolumeName);
263         Log.i(TAG, "Using volume : " + mVolumeName);
264     }
265 
266     /**
267      * Test that we enforce certain media types can only be created in certain directories.
268      */
269     @Test
testTypePathConformity()270     public void testTypePathConformity() throws Exception {
271         final File dcimDir = getDcimDir();
272         final File documentsDir = getDocumentsDir();
273         final File downloadDir = getDownloadDir();
274         final File moviesDir = getMoviesDir();
275         final File musicDir = getMusicDir();
276         final File picturesDir = getPicturesDir();
277         // Only audio files can be created in Music
278         assertThrows(IOException.class, "Operation not permitted",
279                 () -> {
280                     new File(musicDir, NONMEDIA_FILE_NAME).createNewFile();
281                 });
282         assertThrows(IOException.class, "Operation not permitted",
283                 () -> {
284                     new File(musicDir, VIDEO_FILE_NAME).createNewFile();
285                 });
286         assertThrows(IOException.class, "Operation not permitted",
287                 () -> {
288                     new File(musicDir, IMAGE_FILE_NAME).createNewFile();
289                 });
290         // Only video files can be created in Movies
291         assertThrows(IOException.class, "Operation not permitted",
292                 () -> {
293                     new File(moviesDir, NONMEDIA_FILE_NAME).createNewFile();
294                 });
295         assertThrows(IOException.class, "Operation not permitted",
296                 () -> {
297                     new File(moviesDir, AUDIO_FILE_NAME).createNewFile();
298                 });
299         assertThrows(IOException.class, "Operation not permitted",
300                 () -> {
301                     new File(moviesDir, IMAGE_FILE_NAME).createNewFile();
302                 });
303         // Only image and video files can be created in DCIM
304         assertThrows(IOException.class, "Operation not permitted",
305                 () -> {
306                     new File(dcimDir, NONMEDIA_FILE_NAME).createNewFile();
307                 });
308         assertThrows(IOException.class, "Operation not permitted",
309                 () -> {
310                     new File(dcimDir, AUDIO_FILE_NAME).createNewFile();
311                 });
312         // Only image and video files can be created in Pictures
313         assertThrows(IOException.class, "Operation not permitted",
314                 () -> {
315                     new File(picturesDir, NONMEDIA_FILE_NAME).createNewFile();
316                 });
317         assertThrows(IOException.class, "Operation not permitted",
318                 () -> {
319                     new File(picturesDir, AUDIO_FILE_NAME).createNewFile();
320                 });
321         assertThrows(IOException.class, "Operation not permitted",
322                 () -> {
323                     new File(picturesDir, PLAYLIST_FILE_NAME).createNewFile();
324                 });
325         assertThrows(IOException.class, "Operation not permitted",
326                 () -> {
327                     new File(dcimDir, SUBTITLE_FILE_NAME).createNewFile();
328                 });
329 
330         assertCanCreateFile(new File(getAlarmsDir(), AUDIO_FILE_NAME));
331         assertCanCreateFile(new File(getAudiobooksDir(), AUDIO_FILE_NAME));
332         assertCanCreateFile(new File(dcimDir, IMAGE_FILE_NAME));
333         assertCanCreateFile(new File(dcimDir, VIDEO_FILE_NAME));
334         assertCanCreateFile(new File(documentsDir, AUDIO_FILE_NAME));
335         assertCanCreateFile(new File(documentsDir, IMAGE_FILE_NAME));
336         assertCanCreateFile(new File(documentsDir, NONMEDIA_FILE_NAME));
337         assertCanCreateFile(new File(documentsDir, PLAYLIST_FILE_NAME));
338         assertCanCreateFile(new File(documentsDir, SUBTITLE_FILE_NAME));
339         assertCanCreateFile(new File(documentsDir, VIDEO_FILE_NAME));
340         assertCanCreateFile(new File(downloadDir, AUDIO_FILE_NAME));
341         assertCanCreateFile(new File(downloadDir, IMAGE_FILE_NAME));
342         assertCanCreateFile(new File(downloadDir, NONMEDIA_FILE_NAME));
343         assertCanCreateFile(new File(downloadDir, PLAYLIST_FILE_NAME));
344         assertCanCreateFile(new File(downloadDir, SUBTITLE_FILE_NAME));
345         assertCanCreateFile(new File(downloadDir, VIDEO_FILE_NAME));
346         assertCanCreateFile(new File(moviesDir, VIDEO_FILE_NAME));
347         assertCanCreateFile(new File(moviesDir, SUBTITLE_FILE_NAME));
348         assertCanCreateFile(new File(musicDir, AUDIO_FILE_NAME));
349         assertCanCreateFile(new File(musicDir, PLAYLIST_FILE_NAME));
350         assertCanCreateFile(new File(getNotificationsDir(), AUDIO_FILE_NAME));
351         assertCanCreateFile(new File(picturesDir, IMAGE_FILE_NAME));
352         assertCanCreateFile(new File(picturesDir, VIDEO_FILE_NAME));
353         assertCanCreateFile(new File(getPodcastsDir(), AUDIO_FILE_NAME));
354         assertCanCreateFile(new File(getRingtonesDir(), AUDIO_FILE_NAME));
355 
356         // No file whatsoever can be created in the top level directory
357         assertThrows(IOException.class, "Operation not permitted",
358                 () -> {
359                     new File(getExternalStorageDir(), NONMEDIA_FILE_NAME).createNewFile();
360                 });
361         assertThrows(IOException.class, "Operation not permitted",
362                 () -> {
363                     new File(getExternalStorageDir(), AUDIO_FILE_NAME).createNewFile();
364                 });
365         assertThrows(IOException.class, "Operation not permitted",
366                 () -> {
367                     new File(getExternalStorageDir(), IMAGE_FILE_NAME).createNewFile();
368                 });
369         assertThrows(IOException.class, "Operation not permitted",
370                 () -> {
371                     new File(getExternalStorageDir(), VIDEO_FILE_NAME).createNewFile();
372                 });
373     }
374 
375     /**
376      * Test that we enforce certain media types can only be created in certain directories.
377      */
378     @Test
379     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testTypePathConformity_recordingsDir()380     public void testTypePathConformity_recordingsDir() throws Exception {
381         final File recordingsDir = getRecordingsDir();
382 
383         // Only audio files can be created in Recordings
384         assertThrows(IOException.class, "Operation not permitted",
385                 () -> {
386                     new File(recordingsDir, NONMEDIA_FILE_NAME).createNewFile();
387                 });
388         assertThrows(IOException.class, "Operation not permitted",
389                 () -> {
390                     new File(recordingsDir, VIDEO_FILE_NAME).createNewFile();
391                 });
392         assertThrows(IOException.class, "Operation not permitted",
393                 () -> {
394                     new File(recordingsDir, IMAGE_FILE_NAME).createNewFile();
395                 });
396 
397         assertCanCreateFile(new File(recordingsDir, AUDIO_FILE_NAME));
398     }
399 
400     /**
401      * Test that we can create a file in app's external files directory,
402      * and that we can write and read to/from the file.
403      */
404     @Test
testCreateFileInAppExternalDir()405     public void testCreateFileInAppExternalDir() throws Exception {
406         final File file = new File(getExternalFilesDir(), "text.txt");
407         try {
408             assertThat(file.createNewFile()).isTrue();
409             assertThat(file.delete()).isTrue();
410             // Ensure the file is properly deleted and can be created again
411             assertThat(file.createNewFile()).isTrue();
412 
413             // Write to file
414             try (FileOutputStream fos = new FileOutputStream(file)) {
415                 fos.write(BYTES_DATA1);
416             }
417 
418             // Read the same data from file
419             assertFileContent(file, BYTES_DATA1);
420         } finally {
421             file.delete();
422         }
423     }
424 
425     /**
426      * Test that we can't create a file in another app's external files directory,
427      * and that we'll get the same error regardless of whether the app exists or not.
428      */
429     @Test
testCreateFileInOtherAppExternalDir()430     public void testCreateFileInOtherAppExternalDir() throws Exception {
431         // Creating a file in a non existent package dir should return ENOENT, as expected
432         final File nonexistentPackageFileDir = new File(
433                 getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
434         final File file1 = new File(nonexistentPackageFileDir, NONMEDIA_FILE_NAME);
435         assertThrows(
436                 IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> {
437                     file1.createNewFile();
438                 });
439 
440         // Creating a file in an existent package dir should give the same error string to avoid
441         // leaking installed app names, and we know the following directory exists because shell
442         // mkdirs it in test setup
443         final File shellPackageFileDir = new File(
444                 getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
445         final File file2 = new File(shellPackageFileDir, NONMEDIA_FILE_NAME);
446         assertThrows(
447                 IOException.class, FILE_CREATION_ERROR_MESSAGE, () -> {
448                     file1.createNewFile();
449                 });
450     }
451 
452     /**
453      * Test that apps can't read/write files in another app's external files directory,
454      * and can do so in their own app's external file directory.
455      */
456     @Test
testReadWriteFilesInOtherAppExternalDir()457     public void testReadWriteFilesInOtherAppExternalDir() throws Exception {
458         final File videoFile = new File(getExternalFilesDir(), VIDEO_FILE_NAME);
459 
460         try {
461             // Create a file in app's external files directory
462             if (!videoFile.exists()) {
463                 assertThat(videoFile.createNewFile()).isTrue();
464             }
465 
466             // App A should not be able to read/write to other app's external files directory.
467             assertThat(canOpenFileAs(APP_A_HAS_RES, videoFile, false /* forWrite */)).isFalse();
468             assertThat(canOpenFileAs(APP_A_HAS_RES, videoFile, true /* forWrite */)).isFalse();
469             // App A should not be able to delete files in other app's external files
470             // directory.
471             assertThat(deleteFileAs(APP_A_HAS_RES, videoFile.getPath())).isFalse();
472 
473             // Apps should have read/write access in their own app's external files directory.
474             assertThat(canOpen(videoFile, false /* forWrite */)).isTrue();
475             assertThat(canOpen(videoFile, true /* forWrite */)).isTrue();
476             // Apps should be able to delete files in their own app's external files directory.
477             assertThat(videoFile.delete()).isTrue();
478         } finally {
479             videoFile.delete();
480         }
481     }
482 
483     /**
484      * Test that we can contribute media without any permissions.
485      */
486     @Test
testContributeMediaFile()487     public void testContributeMediaFile() throws Exception {
488         final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
489 
490         try {
491             assertThat(imageFile.createNewFile()).isTrue();
492 
493             // Ensure that the file was successfully added to the MediaProvider database
494             assertThat(getFileOwnerPackageFromDatabase(imageFile)).isEqualTo(THIS_PACKAGE_NAME);
495 
496             // Try to write random data to the file
497             try (FileOutputStream fos = new FileOutputStream(imageFile)) {
498                 fos.write(BYTES_DATA1);
499                 fos.write(BYTES_DATA2);
500             }
501 
502             final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
503             assertFileContent(imageFile, expected);
504 
505             // Closing the file after writing will not trigger a MediaScan. Call scanFile to update
506             // file's entry in MediaProvider's database.
507             assertThat(MediaStore.scanFile(getContentResolver(), imageFile)).isNotNull();
508 
509             // Ensure that the scan was completed and the file's size was updated.
510             assertThat(getFileSizeFromDatabase(imageFile)).isEqualTo(
511                     BYTES_DATA1.length + BYTES_DATA2.length);
512         } finally {
513             imageFile.delete();
514         }
515         // Ensure that delete makes a call to MediaProvider to remove the file from its database.
516         assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(-1);
517     }
518 
519     @Test
testCreateAndDeleteEmptyDir()520     public void testCreateAndDeleteEmptyDir() throws Exception {
521         final File externalFilesDir = getExternalFilesDir();
522         // Remove directory in order to create it again
523         externalFilesDir.delete();
524 
525         // Can create own external files dir
526         assertThat(externalFilesDir.mkdir()).isTrue();
527 
528         final File dir1 = new File(externalFilesDir, "random_dir");
529         // Can create dirs inside it
530         assertThat(dir1.mkdir()).isTrue();
531 
532         final File dir2 = new File(dir1, "random_dir_inside_random_dir");
533         // And create a dir inside the new dir
534         assertThat(dir2.mkdir()).isTrue();
535 
536         // And can delete them all
537         assertThat(dir2.delete()).isTrue();
538         assertThat(dir1.delete()).isTrue();
539         assertThat(externalFilesDir.delete()).isTrue();
540 
541         // Can't create external dir for other apps
542         final File nonexistentPackageFileDir = new File(
543                 externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
544         final File shellPackageFileDir = new File(
545                 externalFilesDir.getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
546 
547         assertThat(nonexistentPackageFileDir.mkdir()).isFalse();
548         assertThat(shellPackageFileDir.mkdir()).isFalse();
549     }
550 
551     @Test
testCantAccessOtherAppsContents()552     public void testCantAccessOtherAppsContents() throws Exception {
553         final File mediaFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
554         final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
555         try {
556             assertThat(createFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
557             assertThat(createFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
558 
559             // We can still see that the files exist
560             assertThat(mediaFile.exists()).isTrue();
561             assertThat(nonMediaFile.exists()).isTrue();
562 
563             // But we can't access their content
564             assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
565             assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
566             assertThat(canOpen(mediaFile, /* forWrite */ false)).isFalse();
567             assertThat(canOpen(nonMediaFile, /* forWrite */ true)).isFalse();
568         } finally {
569             deleteFileAsNoThrow(APP_B_NO_PERMS, nonMediaFile.getPath());
570             deleteFileAsNoThrow(APP_B_NO_PERMS, mediaFile.getPath());
571         }
572     }
573 
574     @Test
testCantDeleteOtherAppsContents()575     public void testCantDeleteOtherAppsContents() throws Exception {
576         final File dirInDownload = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
577         final File mediaFile = new File(dirInDownload, IMAGE_FILE_NAME);
578         final File nonMediaFile = new File(dirInDownload, NONMEDIA_FILE_NAME);
579         try {
580             assertThat(dirInDownload.mkdir()).isTrue();
581             // Have another app create a media file in the directory
582             assertThat(createFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
583 
584             // Can't delete the directory since it contains another app's content
585             assertThat(dirInDownload.delete()).isFalse();
586             // Can't delete another app's content
587             assertThat(deleteRecursively(dirInDownload)).isFalse();
588 
589             // Have another app create a non-media file in the directory
590             assertThat(createFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
591 
592             // Can't delete the directory since it contains another app's content
593             assertThat(dirInDownload.delete()).isFalse();
594             // Can't delete another app's content
595             assertThat(deleteRecursively(dirInDownload)).isFalse();
596 
597             // Delete only the media file and keep the non-media file
598             assertThat(deleteFileAs(APP_B_NO_PERMS, mediaFile.getPath())).isTrue();
599             // Directory now has only the non-media file contributed by another app, so we still
600             // can't delete it nor its content
601             assertThat(dirInDownload.delete()).isFalse();
602             assertThat(deleteRecursively(dirInDownload)).isFalse();
603 
604             // Delete the last file belonging to another app
605             assertThat(deleteFileAs(APP_B_NO_PERMS, nonMediaFile.getPath())).isTrue();
606             // Create our own file
607             assertThat(nonMediaFile.createNewFile()).isTrue();
608 
609             // Now that the directory only has content that was contributed by us, we can delete it
610             assertThat(deleteRecursively(dirInDownload)).isTrue();
611         } finally {
612             deleteFileAsNoThrow(APP_B_NO_PERMS, nonMediaFile.getPath());
613             deleteFileAsNoThrow(APP_B_NO_PERMS, mediaFile.getPath());
614             // At this point, we're not sure who created this file, so we'll have both apps
615             // deleting it
616             mediaFile.delete();
617             dirInDownload.delete();
618         }
619     }
620 
621     /**
622      * Test that deleting uri corresponding to a file which was already deleted via filePath
623      * doesn't result in a security exception.
624      */
625     @Test
testDeleteAlreadyUnlinkedFile()626     public void testDeleteAlreadyUnlinkedFile() throws Exception {
627         final File nonMediaFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
628         try {
629             assertTrue(nonMediaFile.createNewFile());
630             final Uri uri = MediaStore.scanFile(getContentResolver(), nonMediaFile);
631             assertNotNull(uri);
632 
633             // Delete the file via filePath
634             assertTrue(nonMediaFile.delete());
635 
636             // If we delete nonMediaFile with ContentResolver#delete, it shouldn't result in a
637             // security exception.
638             assertThat(getContentResolver().delete(uri, Bundle.EMPTY)).isEqualTo(0);
639         } finally {
640             nonMediaFile.delete();
641         }
642     }
643 
644     /**
645      * This test relies on the fact that {@link File#list} uses opendir internally, and that it
646      * returns {@code null} if opendir fails.
647      */
648     @Test
testOpendirRestrictions()649     public void testOpendirRestrictions() throws Exception {
650         // Opening a non existent package directory should fail, as expected
651         final File nonexistentPackageFileDir = new File(
652                 getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "no.such.package"));
653         assertThat(nonexistentPackageFileDir.list()).isNull();
654 
655         // Opening another package's external directory should fail as well, even if it exists
656         final File shellPackageFileDir = new File(
657                 getExternalFilesDir().getPath().replace(THIS_PACKAGE_NAME, "com.android.shell"));
658         assertThat(shellPackageFileDir.list()).isNull();
659 
660         // We can open our own external files directory
661         final String[] filesList = getExternalFilesDir().list();
662         assertThat(filesList).isNotNull();
663 
664         // We can open any public directory in external storage
665         assertThat(getDcimDir().list()).isNotNull();
666         assertThat(getDownloadDir().list()).isNotNull();
667         assertThat(getMoviesDir().list()).isNotNull();
668         assertThat(getMusicDir().list()).isNotNull();
669 
670         // We can open the root directory of external storage
671         final String[] topLevelDirs = getExternalStorageDir().list();
672         assertThat(topLevelDirs).isNotNull();
673         // TODO(b/145287327): This check fails on a device with no visible files.
674         // This can be fixed if we display default directories.
675         // assertThat(topLevelDirs).isNotEmpty();
676     }
677 
678     @Test
testLowLevelFileIO()679     public void testLowLevelFileIO() throws Exception {
680         String filePath = new File(getDownloadDir(), NONMEDIA_FILE_NAME).toString();
681         try {
682             int createFlags = O_CREAT | O_RDWR;
683             int createExclFlags = createFlags | O_EXCL;
684 
685             FileDescriptor fd = Os.open(filePath, createExclFlags, S_IRWXU);
686             Os.close(fd);
687             assertThrows(
688                     ErrnoException.class, () -> {
689                         Os.open(filePath, createExclFlags, S_IRWXU);
690                     });
691 
692             fd = Os.open(filePath, createFlags, S_IRWXU);
693             try {
694                 assertThat(Os.write(fd,
695                         ByteBuffer.wrap(BYTES_DATA1))).isEqualTo(BYTES_DATA1.length);
696                 assertFileContent(fd, BYTES_DATA1);
697             } finally {
698                 Os.close(fd);
699             }
700             // should just append the data
701             fd = Os.open(filePath, createFlags | O_APPEND, S_IRWXU);
702             try {
703                 assertThat(Os.write(fd,
704                         ByteBuffer.wrap(BYTES_DATA2))).isEqualTo(BYTES_DATA2.length);
705                 final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes();
706                 assertFileContent(fd, expected);
707             } finally {
708                 Os.close(fd);
709             }
710             // should overwrite everything
711             fd = Os.open(filePath, createFlags | O_TRUNC, S_IRWXU);
712             try {
713                 final byte[] otherData = "this is different data".getBytes();
714                 assertThat(Os.write(fd, ByteBuffer.wrap(otherData))).isEqualTo(otherData.length);
715                 assertFileContent(fd, otherData);
716             } finally {
717                 Os.close(fd);
718             }
719         } finally {
720             new File(filePath).delete();
721         }
722     }
723 
724     /**
725      * Test that media files from other packages are only visible to apps with storage permission.
726      */
727     @Test
testListDirectoriesWithMediaFiles()728     public void testListDirectoriesWithMediaFiles() throws Exception {
729         final File dcimDir = getDcimDir();
730         final File dir = new File(dcimDir, TEST_DIRECTORY_NAME);
731         final File videoFile = new File(dir, VIDEO_FILE_NAME);
732         final String videoFileName = videoFile.getName();
733         try {
734             if (!dir.exists()) {
735                 assertThat(dir.mkdir()).isTrue();
736             }
737 
738             assertThat(createFileAs(APP_B_NO_PERMS, videoFile.getPath())).isTrue();
739             // App B should see TEST_DIRECTORY in DCIM and new file in TEST_DIRECTORY.
740             assertThat(listAs(APP_B_NO_PERMS, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
741             assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).containsExactly(videoFileName);
742 
743             // App A has storage permission, so should see TEST_DIRECTORY in DCIM and new file
744             // in TEST_DIRECTORY.
745             assertThat(listAs(APP_A_HAS_RES, dcimDir.getPath())).contains(TEST_DIRECTORY_NAME);
746             assertThat(listAs(APP_A_HAS_RES, dir.getPath())).containsExactly(videoFileName);
747 
748             // We are an app without storage permission; should see TEST_DIRECTORY in DCIM and
749             // should not see new file in new TEST_DIRECTORY.
750             assertThat(dcimDir.list()).asList().contains(TEST_DIRECTORY_NAME);
751             assertThat(dir.list()).asList().doesNotContain(videoFileName);
752         } finally {
753             deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile.getPath());
754             dir.delete();
755         }
756     }
757 
758     /**
759      * Test that app can't see non-media files created by other packages
760      */
761     @Test
testListDirectoriesWithNonMediaFiles()762     public void testListDirectoriesWithNonMediaFiles() throws Exception {
763         final File downloadDir = getDownloadDir();
764         final File dir = new File(downloadDir, TEST_DIRECTORY_NAME);
765         final File pdfFile = new File(dir, NONMEDIA_FILE_NAME);
766         final String pdfFileName = pdfFile.getName();
767         try {
768             if (!dir.exists()) {
769                 assertThat(dir.mkdir()).isTrue();
770             }
771 
772             // Have App B create non media file in the new directory.
773             assertThat(createFileAs(APP_B_NO_PERMS, pdfFile.getPath())).isTrue();
774 
775             // App B should see TEST_DIRECTORY in downloadDir and new non media file in
776             // TEST_DIRECTORY.
777             assertThat(listAs(APP_B_NO_PERMS, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
778             assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).containsExactly(pdfFileName);
779 
780             // APP A with storage permission should see TEST_DIRECTORY in downloadDir
781             // and should not see non media file in TEST_DIRECTORY.
782             assertThat(listAs(APP_A_HAS_RES, downloadDir.getPath())).contains(TEST_DIRECTORY_NAME);
783             assertThat(listAs(APP_A_HAS_RES, dir.getPath())).doesNotContain(pdfFileName);
784         } finally {
785             deleteFileAsNoThrow(APP_B_NO_PERMS, pdfFile.getPath());
786             dir.delete();
787         }
788     }
789 
790     /**
791      * Test that app can only see its directory in Android/data.
792      */
793     @Test
testListFilesFromExternalFilesDirectory()794     public void testListFilesFromExternalFilesDirectory() throws Exception {
795         final String packageName = THIS_PACKAGE_NAME;
796         final File nonmediaFile = new File(getExternalFilesDir(), NONMEDIA_FILE_NAME);
797 
798         try {
799             // Create a file in app's external files directory
800             if (!nonmediaFile.exists()) {
801                 assertThat(nonmediaFile.createNewFile()).isTrue();
802             }
803             // App should see its directory and directories of shared packages. App should see all
804             // files and directories in its external directory.
805             assertDirectoryContains(nonmediaFile.getParentFile(), nonmediaFile);
806 
807             // App A should not see other app's external files directory despite RES.
808             assertThrows(IOException.class,
809                     () -> listAs(APP_A_HAS_RES, getAndroidDataDir().getPath()));
810             assertThrows(IOException.class,
811                     () -> listAs(APP_A_HAS_RES, getExternalFilesDir().getPath()));
812         } finally {
813             nonmediaFile.delete();
814         }
815     }
816 
817     /**
818      * Test that app can see files and directories in Android/media.
819      */
820     @Test
testListFilesFromExternalMediaDirectory()821     public void testListFilesFromExternalMediaDirectory() throws Exception {
822         final File videoFile = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
823 
824         try {
825             // Create a file in app's external media directory
826             if (!videoFile.exists()) {
827                 assertThat(videoFile.createNewFile()).isTrue();
828             }
829 
830             // App should see its directory and other app's external media directories with media
831             // files.
832             assertDirectoryContains(videoFile.getParentFile(), videoFile);
833 
834             // App A with storage permission should see other app's external media directory.
835             // Apps with READ_EXTERNAL_STORAGE can list files in other app's external media
836             // directory.
837             assertThat(listAs(APP_A_HAS_RES, getAndroidMediaDir().getPath()))
838                     .contains(THIS_PACKAGE_NAME);
839             assertThat(listAs(APP_A_HAS_RES, getExternalMediaDir().getPath()))
840                     .containsExactly(videoFile.getName());
841         } finally {
842             videoFile.delete();
843         }
844     }
845 
846     @Test
testMetaDataRedaction()847     public void testMetaDataRedaction() throws Exception {
848         File jpgFile = new File(getPicturesDir(), "img_metadata.jpg");
849         try {
850             if (jpgFile.exists()) {
851                 assertThat(jpgFile.delete()).isTrue();
852             }
853 
854             HashMap<String, String> originalExif =
855                     getExifMetadataFromRawResource(R.raw.img_with_metadata);
856 
857             try (InputStream in =
858                          getContext().getResources().openRawResource(R.raw.img_with_metadata);
859                 FileOutputStream out = new FileOutputStream(jpgFile)) {
860                 // Dump the image we have to external storage
861                 FileUtils.copy(in, out);
862                 // Sync file to disk to ensure file is fully written to the lower fs attempting to
863                 // open for redaction. Otherwise, the FUSE daemon might not accurately parse the
864                 // EXIF tags and might misleadingly think there are not tags to redact
865                 out.getFD().sync();
866 
867                 HashMap<String, String> exif = getExifMetadata(jpgFile);
868                 assertExifMetadataMatch(exif, originalExif);
869 
870                 HashMap<String, String> exifFromTestApp =
871                         readExifMetadataFromTestApp(APP_A_HAS_RES, jpgFile.getPath());
872                 // App does not have AML; shouldn't have access to the same metadata.
873                 assertExifMetadataMismatch(exifFromTestApp, originalExif);
874 
875                 // TODO(b/146346138): Test that if we give APP_A write URI permission,
876                 //  it would be able to access the metadata.
877             } // Intentionally keep the original streams open during the test so bytes are more
878             // likely to be in the VFS cache from both file opens
879         } finally {
880             jpgFile.delete();
881         }
882     }
883 
884     @Test
testOpenFilePathFirstWriteContentResolver()885     public void testOpenFilePathFirstWriteContentResolver() throws Exception {
886         String displayName = "open_file_path_write_content_resolver.jpg";
887         File file = new File(getDcimDir(), displayName);
888 
889         try {
890             assertThat(file.createNewFile()).isTrue();
891 
892             ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
893             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
894 
895             assertRWR(readPfd, writePfd);
896             assertUpperFsFd(writePfd); // With cache
897         } finally {
898             file.delete();
899         }
900     }
901 
902     @Test
testOpenContentResolverFirstWriteContentResolver()903     public void testOpenContentResolverFirstWriteContentResolver() throws Exception {
904         String displayName = "open_content_resolver_write_content_resolver.jpg";
905         File file = new File(getDcimDir(), displayName);
906 
907         try {
908             assertThat(file.createNewFile()).isTrue();
909 
910             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
911             ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
912 
913             assertRWR(readPfd, writePfd);
914             assertLowerFsFdWithPassthrough(writePfd);
915         } finally {
916             file.delete();
917         }
918     }
919 
920     @Test
testOpenFilePathFirstWriteFilePath()921     public void testOpenFilePathFirstWriteFilePath() throws Exception {
922         String displayName = "open_file_path_write_file_path.jpg";
923         File file = new File(getDcimDir(), displayName);
924 
925         try {
926             assertThat(file.createNewFile()).isTrue();
927 
928             ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
929             ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
930 
931             assertRWR(readPfd, writePfd);
932             assertUpperFsFd(readPfd); // With cache
933         } finally {
934             file.delete();
935         }
936     }
937 
938     @Test
testOpenContentResolverFirstWriteFilePath()939     public void testOpenContentResolverFirstWriteFilePath() throws Exception {
940         String displayName = "open_content_resolver_write_file_path.jpg";
941         File file = new File(getDcimDir(), displayName);
942 
943         try {
944             assertThat(file.createNewFile()).isTrue();
945 
946             ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
947             ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
948 
949             assertRWR(readPfd, writePfd);
950             assertLowerFsFdWithPassthrough(readPfd);
951         } finally {
952             file.delete();
953         }
954     }
955 
956     @Test
testOpenContentResolverWriteOnly()957     public void testOpenContentResolverWriteOnly() throws Exception {
958         String displayName = "open_content_resolver_write_only.jpg";
959         File file = new File(getDcimDir(), displayName);
960 
961         try {
962             assertThat(file.createNewFile()).isTrue();
963 
964             // We upgrade 'w' only to 'rw'
965             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
966             ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
967 
968             assertRWR(readPfd, writePfd);
969             assertRWR(writePfd, readPfd); // Can read on 'w' only pfd
970             assertLowerFsFdWithPassthrough(writePfd);
971             assertLowerFsFdWithPassthrough(readPfd);
972         } finally {
973             file.delete();
974         }
975     }
976 
977     @Test
testOpenContentResolverDup()978     public void testOpenContentResolverDup() throws Exception {
979         String displayName = "open_content_resolver_dup.jpg";
980         File file = new File(getDcimDir(), displayName);
981 
982         try {
983             file.delete();
984             assertThat(file.createNewFile()).isTrue();
985 
986             // Even if we close the original fd, since we have a dup open
987             // the FUSE IO should still bypass the cache
988             try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw")) {
989                 try (ParcelFileDescriptor writePfdDup = writePfd.dup();
990                      ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(
991                              file, MODE_READ_WRITE)) {
992                     writePfd.close();
993 
994                     assertRWR(readPfd, writePfdDup);
995                     assertLowerFsFdWithPassthrough(writePfdDup);
996                 }
997             }
998         } finally {
999             file.delete();
1000         }
1001     }
1002 
1003     @Test
testOpenContentResolverClose()1004     public void testOpenContentResolverClose() throws Exception {
1005         String displayName = "open_content_resolver_close.jpg";
1006         File file = new File(getDcimDir(), displayName);
1007 
1008         try {
1009             byte[] readBuffer = new byte[10];
1010             byte[] writeBuffer = new byte[10];
1011             Arrays.fill(writeBuffer, (byte) 1);
1012 
1013             assertThat(file.createNewFile()).isTrue();
1014 
1015             // Lower fs open and write
1016             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
1017             Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
1018 
1019             // Close so upper fs open will not use direct_io
1020             writePfd.close();
1021 
1022             // Upper fs open and read without direct_io
1023             ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE);
1024             Os.pread(readPfd.getFileDescriptor(), readBuffer, 0, 10, 0);
1025 
1026             // Last write on lower fs is visible via upper fs
1027             assertThat(readBuffer).isEqualTo(writeBuffer);
1028             assertThat(readPfd.getStatSize()).isEqualTo(writeBuffer.length);
1029         } finally {
1030             file.delete();
1031         }
1032     }
1033 
1034     @Test
testContentResolverDelete()1035     public void testContentResolverDelete() throws Exception {
1036         String displayName = "content_resolver_delete.jpg";
1037         File file = new File(getDcimDir(), displayName);
1038 
1039         try {
1040             assertThat(file.createNewFile()).isTrue();
1041 
1042             deleteWithMediaProvider(file);
1043 
1044             assertThat(file.exists()).isFalse();
1045             assertThat(file.createNewFile()).isTrue();
1046         } finally {
1047             file.delete();
1048         }
1049     }
1050 
1051     @Test
testContentResolverUpdate()1052     public void testContentResolverUpdate() throws Exception {
1053         String oldDisplayName = "content_resolver_update_old.jpg";
1054         String newDisplayName = "content_resolver_update_new.jpg";
1055         File oldFile = new File(getDcimDir(), oldDisplayName);
1056         File newFile = new File(getDcimDir(), newDisplayName);
1057 
1058         try {
1059             assertThat(oldFile.createNewFile()).isTrue();
1060             // Publish the pending oldFile before updating with MediaProvider. Not publishing the
1061             // file will make MP consider pending from FUSE as explicit IS_PENDING
1062             final Uri uri = MediaStore.scanFile(getContentResolver(), oldFile);
1063             assertNotNull(uri);
1064 
1065             updateDisplayNameWithMediaProvider(uri,
1066                     Environment.DIRECTORY_DCIM, oldDisplayName, newDisplayName);
1067 
1068             assertThat(oldFile.exists()).isFalse();
1069             assertThat(oldFile.createNewFile()).isTrue();
1070             assertThat(newFile.exists()).isTrue();
1071             assertThat(newFile.createNewFile()).isFalse();
1072         } finally {
1073             oldFile.delete();
1074             newFile.delete();
1075         }
1076     }
1077 
1078     @Test
1079     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testDefaultNoIsolatedStorageFlag()1080     public void testDefaultNoIsolatedStorageFlag() throws Exception {
1081         assertThat(Environment.isExternalStorageLegacy()).isFalse();
1082     }
1083 
1084     @Test
testCreateLowerCaseDeleteUpperCase()1085     public void testCreateLowerCaseDeleteUpperCase() throws Exception {
1086         File upperCase = new File(getDownloadDir(), "CREATE_LOWER_DELETE_UPPER");
1087         File lowerCase = new File(getDownloadDir(), "create_lower_delete_upper");
1088 
1089         createDeleteCreate(lowerCase, upperCase);
1090     }
1091 
1092     @Test
testCreateUpperCaseDeleteLowerCase()1093     public void testCreateUpperCaseDeleteLowerCase() throws Exception {
1094         File upperCase = new File(getDownloadDir(), "CREATE_UPPER_DELETE_LOWER");
1095         File lowerCase = new File(getDownloadDir(), "create_upper_delete_lower");
1096 
1097         createDeleteCreate(upperCase, lowerCase);
1098     }
1099 
1100     @Test
testCreateMixedCaseDeleteDifferentMixedCase()1101     public void testCreateMixedCaseDeleteDifferentMixedCase() throws Exception {
1102         File mixedCase1 = new File(getDownloadDir(), "CrEaTe_MiXeD_dElEtE_mIxEd");
1103         File mixedCase2 = new File(getDownloadDir(), "cReAtE_mIxEd_DeLeTe_MiXeD");
1104 
1105         createDeleteCreate(mixedCase1, mixedCase2);
1106     }
1107 
1108     @Test
testAndroidDataObbDoesNotForgetMount()1109     public void testAndroidDataObbDoesNotForgetMount() throws Exception {
1110         File dataDir = getContext().getExternalFilesDir(null);
1111         File upperCaseDataDir = new File(dataDir.getPath().replace("Android/data", "ANDROID/DATA"));
1112 
1113         File obbDir = getContext().getObbDir();
1114         File upperCaseObbDir = new File(obbDir.getPath().replace("Android/obb", "ANDROID/OBB"));
1115 
1116 
1117         StructStat beforeDataStruct = Os.stat(dataDir.getPath());
1118         StructStat beforeObbStruct = Os.stat(obbDir.getPath());
1119 
1120         assertThat(dataDir.exists()).isTrue();
1121         assertThat(upperCaseDataDir.exists()).isTrue();
1122         assertThat(obbDir.exists()).isTrue();
1123         assertThat(upperCaseObbDir.exists()).isTrue();
1124 
1125         StructStat afterDataStruct = Os.stat(upperCaseDataDir.getPath());
1126         StructStat afterObbStruct = Os.stat(upperCaseObbDir.getPath());
1127 
1128         assertThat(beforeDataStruct.st_dev).isEqualTo(afterDataStruct.st_dev);
1129         assertThat(beforeObbStruct.st_dev).isEqualTo(afterObbStruct.st_dev);
1130     }
1131 
1132     @Test
testCacheConsistencyForCaseInsensitivity()1133     public void testCacheConsistencyForCaseInsensitivity() throws Exception {
1134         File upperCaseFile = new File(getDownloadDir(), "CACHE_CONSISTENCY_FOR_CASE_INSENSITIVITY");
1135         File lowerCaseFile = new File(getDownloadDir(), "cache_consistency_for_case_insensitivity");
1136 
1137         try {
1138             ParcelFileDescriptor upperCasePfd =
1139                     ParcelFileDescriptor.open(upperCaseFile, MODE_READ_WRITE | MODE_CREATE);
1140             ParcelFileDescriptor lowerCasePfd =
1141                     ParcelFileDescriptor.open(lowerCaseFile, MODE_READ_WRITE | MODE_CREATE);
1142 
1143             assertRWR(upperCasePfd, lowerCasePfd);
1144             assertRWR(lowerCasePfd, upperCasePfd);
1145         } finally {
1146             upperCaseFile.delete();
1147             lowerCaseFile.delete();
1148         }
1149     }
1150 
1151     @Test
testInsertDefaultPrimaryCaseInsensitiveCheck()1152     public void testInsertDefaultPrimaryCaseInsensitiveCheck() throws Exception {
1153         final File podcastsDir = getPodcastsDir();
1154         final File podcastsDirLowerCase =
1155                 new File(getExternalStorageDir(), Environment.DIRECTORY_PODCASTS.toLowerCase());
1156         final File fileInPodcastsDirLowerCase = new File(podcastsDirLowerCase, AUDIO_FILE_NAME);
1157         try {
1158             // Delete the directory if it already exists
1159             if (podcastsDir.exists()) {
1160                 deleteAsLegacyApp(podcastsDir);
1161             }
1162             assertThat(podcastsDir.exists()).isFalse();
1163             assertThat(podcastsDirLowerCase.exists()).isFalse();
1164 
1165             // Create the directory with lower case
1166             assertThat(podcastsDirLowerCase.mkdir()).isTrue();
1167             // Because of case-insensitivity, even though directory is created
1168             // with lower case, we should be able to see both directory names.
1169             assertThat(podcastsDirLowerCase.exists()).isTrue();
1170             assertThat(podcastsDir.exists()).isTrue();
1171 
1172             // File creation with lower case path of podcasts directory should not fail
1173             assertThat(fileInPodcastsDirLowerCase.createNewFile()).isTrue();
1174         } finally {
1175             fileInPodcastsDirLowerCase.delete();
1176             deleteAsLegacyApp(podcastsDirLowerCase);
1177             podcastsDir.mkdirs();
1178         }
1179     }
1180 
createDeleteCreate(File create, File delete)1181     private void createDeleteCreate(File create, File delete) throws Exception {
1182         try {
1183             assertThat(create.createNewFile()).isTrue();
1184             // Wait for the kernel to update the dentry cache.
1185             Thread.sleep(100);
1186 
1187             assertThat(delete.delete()).isTrue();
1188             // Wait for the kernel to clean up the dentry cache.
1189             Thread.sleep(100);
1190 
1191             assertThat(create.createNewFile()).isTrue();
1192             // Wait for the kernel to update the dentry cache.
1193             Thread.sleep(100);
1194         } finally {
1195             create.delete();
1196             delete.delete();
1197         }
1198     }
1199 
1200     @Test
testReadStorageInvalidation()1201     public void testReadStorageInvalidation() throws Exception {
1202         testAppOpInvalidation(APP_C, new File(getDcimDir(), "read_storage.jpg"),
1203                 Manifest.permission.READ_EXTERNAL_STORAGE,
1204                 AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
1205     }
1206 
1207     @Test
testWriteStorageInvalidation()1208     public void testWriteStorageInvalidation() throws Exception {
1209         testAppOpInvalidation(APP_C_LEGACY, new File(getDcimDir(), "write_storage.jpg"),
1210                 Manifest.permission.WRITE_EXTERNAL_STORAGE,
1211                 AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, /* forWrite */ true);
1212     }
1213 
1214     @Test
testManageStorageInvalidation()1215     public void testManageStorageInvalidation() throws Exception {
1216         testAppOpInvalidation(APP_C, new File(getDownloadDir(), "manage_storage.pdf"),
1217                 /* permission */ null, OPSTR_MANAGE_EXTERNAL_STORAGE, /* forWrite */ true);
1218     }
1219 
1220     @Test
testWriteImagesInvalidation()1221     public void testWriteImagesInvalidation() throws Exception {
1222         testAppOpInvalidation(APP_C, new File(getDcimDir(), "write_images.jpg"),
1223                 /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, /* forWrite */ true);
1224     }
1225 
1226     @Test
testWriteVideoInvalidation()1227     public void testWriteVideoInvalidation() throws Exception {
1228         testAppOpInvalidation(APP_C, new File(getDcimDir(), "write_video.mp4"),
1229                 /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, /* forWrite */ true);
1230     }
1231 
1232     @Test
testAccessMediaLocationInvalidation()1233     public void testAccessMediaLocationInvalidation() throws Exception {
1234         File imgFile = new File(getDcimDir(), "access_media_location.jpg");
1235 
1236         try {
1237             // Setup image with sensitive data on external storage
1238             HashMap<String, String> originalExif =
1239                     getExifMetadataFromRawResource(R.raw.img_with_metadata);
1240             try (InputStream in =
1241                          getContext().getResources().openRawResource(R.raw.img_with_metadata);
1242                 FileOutputStream out = new FileOutputStream(imgFile)) {
1243                 // Dump the image we have to external storage
1244                 FileUtils.copy(in, out);
1245                 // Sync file to disk to ensure file is fully written to the lower fs.
1246                 out.getFD().sync();
1247             }
1248             HashMap<String, String> exif = getExifMetadata(imgFile);
1249             assertExifMetadataMatch(exif, originalExif);
1250 
1251             // Install test app
1252             installAppWithStoragePermissions(APP_C);
1253 
1254             // Grant A_M_L and verify access to sensitive data
1255             grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
1256             HashMap<String, String> exifFromTestApp =
1257                     readExifMetadataFromTestApp(APP_C, imgFile.getPath());
1258             assertExifMetadataMatch(exifFromTestApp, originalExif);
1259 
1260             // Revoke A_M_L and verify sensitive data redaction
1261             revokePermission(
1262                     APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
1263             // revokePermission waits for permission status to be updated, but MediaProvider still
1264             // needs to get permission change callback and clear its permission cache.
1265             Thread.sleep(500);
1266             exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
1267             assertExifMetadataMismatch(exifFromTestApp, originalExif);
1268 
1269             // Re-grant A_M_L and verify access to sensitive data
1270             grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
1271             // grantPermission waits for permission status to be updated, but MediaProvider still
1272             // needs to get permission change callback and clear its permission cache.
1273             Thread.sleep(500);
1274             exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
1275             assertExifMetadataMatch(exifFromTestApp, originalExif);
1276         } finally {
1277             imgFile.delete();
1278             uninstallAppNoThrow(APP_C);
1279         }
1280     }
1281 
1282     @Test
testAppUpdateInvalidation()1283     public void testAppUpdateInvalidation() throws Exception {
1284         File file = new File(getDcimDir(), "app_update.jpg");
1285         try {
1286             assertThat(file.createNewFile()).isTrue();
1287 
1288             // Install legacy
1289             installAppWithStoragePermissions(APP_C_LEGACY);
1290             grantPermission(APP_C_LEGACY.getPackageName(),
1291                     Manifest.permission.WRITE_EXTERNAL_STORAGE); // Grants write access for legacy
1292 
1293             // Legacy app can read and write media files contributed by others
1294             assertThat(canOpenFileAs(APP_C_LEGACY, file, /* forWrite */ false)).isTrue();
1295             assertThat(canOpenFileAs(APP_C_LEGACY, file, /* forWrite */ true)).isTrue();
1296 
1297             // Update to non-legacy
1298             installAppWithStoragePermissions(APP_C);
1299             grantPermission(APP_C_LEGACY.getPackageName(),
1300                     Manifest.permission.WRITE_EXTERNAL_STORAGE); // No effect for non-legacy
1301 
1302             // Non-legacy app can read media files contributed by others
1303             assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isTrue();
1304             // But cannot write
1305             assertThat(canOpenFileAs(APP_C, file, /* forWrite */ true)).isFalse();
1306         } finally {
1307             file.delete();
1308             uninstallAppNoThrow(APP_C);
1309         }
1310     }
1311 
1312     @Test
testAppReinstallInvalidation()1313     public void testAppReinstallInvalidation() throws Exception {
1314         File file = new File(getDcimDir(), "app_reinstall.jpg");
1315 
1316         try {
1317             assertThat(file.createNewFile()).isTrue();
1318 
1319             // Install
1320             installAppWithStoragePermissions(APP_C);
1321             assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isTrue();
1322 
1323             // Re-install
1324             uninstallAppNoThrow(APP_C);
1325             installApp(APP_C);
1326             assertThat(canOpenFileAs(APP_C, file, /* forWrite */ false)).isFalse();
1327         } finally {
1328             file.delete();
1329             uninstallAppNoThrow(APP_C);
1330         }
1331     }
1332 
testAppOpInvalidation(TestApp app, File file, @Nullable String permission, String opstr, boolean forWrite)1333     private void testAppOpInvalidation(TestApp app, File file, @Nullable String permission,
1334             String opstr, boolean forWrite) throws Exception {
1335         boolean alreadyInstalled = true;
1336         try {
1337             if (!isAppInstalled(app)) {
1338                 alreadyInstalled = false;
1339                 installApp(app);
1340             }
1341             assertThat(file.createNewFile()).isTrue();
1342             assertAppOpInvalidation(app, file, permission, opstr, forWrite);
1343         } finally {
1344             file.delete();
1345             if (!alreadyInstalled) {
1346                 // only uninstall if we installed this app here
1347                 uninstallApp(app);
1348             }
1349         }
1350     }
1351 
1352     /** If {@code permission} is null, appops are flipped, otherwise permissions are flipped */
assertAppOpInvalidation(TestApp app, File file, @Nullable String permission, String opstr, boolean forWrite)1353     private void assertAppOpInvalidation(TestApp app, File file, @Nullable String permission,
1354             String opstr, boolean forWrite) throws Exception {
1355         String packageName = app.getPackageName();
1356         int uid = getContext().getPackageManager().getPackageUid(packageName, 0);
1357 
1358         // Deny
1359         if (permission != null) {
1360             revokePermission(packageName, permission);
1361         } else {
1362             denyAppOpsToUid(uid, opstr);
1363             // TODO(191724755): Poll for AppOp state change instead
1364             Thread.sleep(200);
1365         }
1366         assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
1367 
1368         // Grant
1369         if (permission != null) {
1370             grantPermission(packageName, permission);
1371         } else {
1372             allowAppOpsToUid(uid, opstr);
1373             // TODO(191724755): Poll for AppOp state change instead
1374             Thread.sleep(200);
1375         }
1376         assertThat(canOpenFileAs(app, file, forWrite)).isTrue();
1377 
1378         // Deny
1379         if (permission != null) {
1380             revokePermission(packageName, permission);
1381         } else {
1382             denyAppOpsToUid(uid, opstr);
1383             // TODO(191724755): Poll for AppOp state change instead
1384             Thread.sleep(200);
1385         }
1386         assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
1387     }
1388 
1389     @Test
1390     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testDisableOpResetForSystemGallery()1391     public void testDisableOpResetForSystemGallery() throws Exception {
1392         final File otherAppImageFile = new File(getDcimDir(), "other_" + IMAGE_FILE_NAME);
1393         final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
1394 
1395         try {
1396             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1397 
1398             // Have another app create an image file
1399             assertThat(createFileAs(APP_B_NO_PERMS, otherAppImageFile.getPath())).isTrue();
1400             assertThat(otherAppImageFile.exists()).isTrue();
1401 
1402             // Have another app create a video file
1403             assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
1404             assertThat(otherAppVideoFile.exists()).isTrue();
1405 
1406             assertCanWriteAndRead(otherAppImageFile, BYTES_DATA1);
1407             assertCanWriteAndRead(otherAppVideoFile, BYTES_DATA1);
1408 
1409             // Reset app op should not reset System Gallery privileges
1410             executeShellCommand("appops reset " + THIS_PACKAGE_NAME);
1411 
1412             // Assert we can still write to images/videos
1413             assertCanWriteAndRead(otherAppImageFile, BYTES_DATA2);
1414             assertCanWriteAndRead(otherAppVideoFile, BYTES_DATA2);
1415 
1416         } finally {
1417             deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImageFile.getAbsolutePath());
1418             deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppVideoFile.getAbsolutePath());
1419             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1420         }
1421     }
1422 
1423     @Test
testSystemGalleryAppHasFullAccessToImages()1424     public void testSystemGalleryAppHasFullAccessToImages() throws Exception {
1425         final File otherAppImageFile = new File(getDcimDir(), "other_" + IMAGE_FILE_NAME);
1426         final File topLevelImageFile = new File(getExternalStorageDir(), IMAGE_FILE_NAME);
1427         final File imageInAnObviouslyWrongPlace = new File(getMusicDir(), IMAGE_FILE_NAME);
1428 
1429         try {
1430             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1431 
1432             // Have another app create an image file
1433             assertThat(createFileAs(APP_B_NO_PERMS, otherAppImageFile.getPath())).isTrue();
1434             assertThat(otherAppImageFile.exists()).isTrue();
1435 
1436             // Assert we can write to the file
1437             try (FileOutputStream fos = new FileOutputStream(otherAppImageFile)) {
1438                 fos.write(BYTES_DATA1);
1439             }
1440 
1441             // Assert we can read from the file
1442             assertFileContent(otherAppImageFile, BYTES_DATA1);
1443 
1444             // Assert we can delete the file
1445             assertThat(otherAppImageFile.delete()).isTrue();
1446             assertThat(otherAppImageFile.exists()).isFalse();
1447 
1448             // Can create an image anywhere
1449             assertCanCreateFile(topLevelImageFile);
1450             assertCanCreateFile(imageInAnObviouslyWrongPlace);
1451 
1452             // Put the file back in its place and let APP B delete it
1453             assertThat(otherAppImageFile.createNewFile()).isTrue();
1454         } finally {
1455             deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImageFile.getAbsolutePath());
1456             otherAppImageFile.delete();
1457             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1458         }
1459     }
1460 
1461     @Test
testSystemGalleryAppHasNoFullAccessToAudio()1462     public void testSystemGalleryAppHasNoFullAccessToAudio() throws Exception {
1463         final File otherAppAudioFile = new File(getMusicDir(), "other_" + AUDIO_FILE_NAME);
1464         final File topLevelAudioFile = new File(getExternalStorageDir(), AUDIO_FILE_NAME);
1465         final File audioInAnObviouslyWrongPlace = new File(getPicturesDir(), AUDIO_FILE_NAME);
1466 
1467         try {
1468             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1469 
1470             // Have another app create an audio file
1471             assertThat(createFileAs(APP_B_NO_PERMS, otherAppAudioFile.getPath())).isTrue();
1472             assertThat(otherAppAudioFile.exists()).isTrue();
1473 
1474             // Assert we can't access the file
1475             assertThat(canOpen(otherAppAudioFile, /* forWrite */ false)).isFalse();
1476             assertThat(canOpen(otherAppAudioFile, /* forWrite */ true)).isFalse();
1477 
1478             // Assert we can't delete the file
1479             assertThat(otherAppAudioFile.delete()).isFalse();
1480 
1481             // Can't create an audio file where it doesn't belong
1482             assertThrows(IOException.class, "Operation not permitted",
1483                     () -> {
1484                         topLevelAudioFile.createNewFile();
1485                     });
1486             assertThrows(IOException.class, "Operation not permitted",
1487                     () -> {
1488                         audioInAnObviouslyWrongPlace.createNewFile();
1489                     });
1490         } finally {
1491             deleteFileAs(APP_B_NO_PERMS, otherAppAudioFile.getPath());
1492             topLevelAudioFile.delete();
1493             audioInAnObviouslyWrongPlace.delete();
1494             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1495         }
1496     }
1497 
1498     @Test
testSystemGalleryCanRenameImagesAndVideos()1499     public void testSystemGalleryCanRenameImagesAndVideos() throws Exception {
1500         final File otherAppVideoFile = new File(getDcimDir(), "other_" + VIDEO_FILE_NAME);
1501         final File imageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
1502         final File videoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
1503         final File topLevelVideoFile = new File(getExternalStorageDir(), VIDEO_FILE_NAME);
1504         final File musicFile = new File(getMusicDir(), AUDIO_FILE_NAME);
1505         try {
1506             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1507 
1508             // Have another app create a video file
1509             assertThat(createFileAs(APP_B_NO_PERMS, otherAppVideoFile.getPath())).isTrue();
1510             assertThat(otherAppVideoFile.exists()).isTrue();
1511 
1512             // Write some data to the file
1513             try (FileOutputStream fos = new FileOutputStream(otherAppVideoFile)) {
1514                 fos.write(BYTES_DATA1);
1515             }
1516             assertFileContent(otherAppVideoFile, BYTES_DATA1);
1517 
1518             // Assert we can rename the file and ensure the file has the same content
1519             assertCanRenameFile(otherAppVideoFile, videoFile);
1520             assertFileContent(videoFile, BYTES_DATA1);
1521             // We can even move it to the top level directory
1522             assertCanRenameFile(videoFile, topLevelVideoFile);
1523             assertFileContent(topLevelVideoFile, BYTES_DATA1);
1524             // And we can even convert it into an image file, because why not?
1525             assertCanRenameFile(topLevelVideoFile, imageFile);
1526             assertFileContent(imageFile, BYTES_DATA1);
1527 
1528             // We can convert it to a music file, but we won't have access to music file after
1529             // renaming.
1530             assertThat(imageFile.renameTo(musicFile)).isTrue();
1531             assertThat(getFileRowIdFromDatabase(musicFile)).isEqualTo(-1);
1532         } finally {
1533             deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppVideoFile.getAbsolutePath());
1534             imageFile.delete();
1535             videoFile.delete();
1536             topLevelVideoFile.delete();
1537             executeShellCommand("rm  " + musicFile.getAbsolutePath());
1538             MediaStore.scanFile(getContentResolver(), musicFile);
1539             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
1540         }
1541     }
1542 
1543     /**
1544      * Test that basic file path restrictions are enforced on file rename.
1545      */
1546     @Test
testRenameFile()1547     public void testRenameFile() throws Exception {
1548         final File downloadDir = getDownloadDir();
1549         final File nonMediaDir = new File(downloadDir, TEST_DIRECTORY_NAME);
1550         final File pdfFile1 = new File(downloadDir, NONMEDIA_FILE_NAME);
1551         final File pdfFile2 = new File(nonMediaDir, NONMEDIA_FILE_NAME);
1552         final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
1553         final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
1554         final File videoFile3 = new File(downloadDir, VIDEO_FILE_NAME);
1555 
1556         try {
1557             // Renaming non media file to media directory is not allowed.
1558             assertThat(pdfFile1.createNewFile()).isTrue();
1559             assertCantRenameFile(pdfFile1, new File(getDcimDir(), NONMEDIA_FILE_NAME));
1560             assertCantRenameFile(pdfFile1, new File(getMusicDir(), NONMEDIA_FILE_NAME));
1561             assertCantRenameFile(pdfFile1, new File(getMoviesDir(), NONMEDIA_FILE_NAME));
1562 
1563             // Renaming non media files to non media directories is allowed.
1564             if (!nonMediaDir.exists()) {
1565                 assertThat(nonMediaDir.mkdirs()).isTrue();
1566             }
1567             // App can rename pdfFile to non media directory.
1568             assertCanRenameFile(pdfFile1, pdfFile2);
1569 
1570             assertThat(videoFile1.createNewFile()).isTrue();
1571             // App can rename video file to Movies directory
1572             assertCanRenameFile(videoFile1, videoFile2);
1573             // App can rename video file to Download directory
1574             assertCanRenameFile(videoFile2, videoFile3);
1575         } finally {
1576             pdfFile1.delete();
1577             pdfFile2.delete();
1578             videoFile1.delete();
1579             videoFile2.delete();
1580             videoFile3.delete();
1581             nonMediaDir.delete();
1582         }
1583     }
1584 
1585     /**
1586      * Test that renaming file to different mime type is allowed.
1587      */
1588     @Test
testRenameFileType()1589     public void testRenameFileType() throws Exception {
1590         final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
1591         final File videoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
1592         try {
1593             assertThat(pdfFile.createNewFile()).isTrue();
1594             assertThat(videoFile.exists()).isFalse();
1595             // Moving pdfFile to DCIM directory is not allowed.
1596             assertCantRenameFile(pdfFile, new File(getDcimDir(), NONMEDIA_FILE_NAME));
1597             // However, moving pdfFile to DCIM directory with changing the mime type to video is
1598             // allowed.
1599             assertCanRenameFile(pdfFile, videoFile);
1600 
1601             // On rename, MediaProvider database entry for pdfFile should be updated with new
1602             // videoFile path and mime type should be updated to video/mp4.
1603             assertThat(getFileMimeTypeFromDatabase(videoFile)).isEqualTo("video/mp4");
1604         } finally {
1605             pdfFile.delete();
1606             videoFile.delete();
1607         }
1608     }
1609 
1610     /**
1611      * Test that renaming files overwrites files in newPath.
1612      */
1613     @Test
testRenameAndReplaceFile()1614     public void testRenameAndReplaceFile() throws Exception {
1615         final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
1616         final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
1617         final ContentResolver cr = getContentResolver();
1618         try {
1619             assertThat(videoFile1.createNewFile()).isTrue();
1620             assertThat(videoFile2.createNewFile()).isTrue();
1621             final Uri uriVideoFile1 = MediaStore.scanFile(cr, videoFile1);
1622             final Uri uriVideoFile2 = MediaStore.scanFile(cr, videoFile2);
1623 
1624             // Renaming a file which replaces file in newPath videoFile2 is allowed.
1625             assertCanRenameFile(videoFile1, videoFile2);
1626 
1627             // Uri of videoFile2 should be accessible after rename.
1628             assertThat(cr.openFileDescriptor(uriVideoFile2, "rw")).isNotNull();
1629             // Uri of videoFile1 should not be accessible after rename.
1630             assertThrows(FileNotFoundException.class,
1631                     () -> {
1632                         cr.openFileDescriptor(uriVideoFile1, "rw");
1633                     });
1634         } finally {
1635             videoFile1.delete();
1636             videoFile2.delete();
1637         }
1638     }
1639 
1640     /**
1641      * Test that ScanFile() after renaming file extension updates the right
1642      * MIME type from the file metadata.
1643      */
1644     @Test
testScanUpdatesMimeTypeForRenameFileExtension()1645     public void testScanUpdatesMimeTypeForRenameFileExtension() throws Exception {
1646         final String audioFileName = "ScopedStorageDeviceTest_" + NONCE;
1647         final File mpegFile = new File(getMusicDir(), audioFileName + ".mp3");
1648         final File nonMpegFile = new File(getMusicDir(), audioFileName + ".snd");
1649         try {
1650             // Copy audio content to mpegFile
1651             try (InputStream in =
1652                          getContext().getResources().openRawResource(R.raw.test_audio);
1653                  FileOutputStream out = new FileOutputStream(mpegFile)) {
1654                 FileUtils.copy(in, out);
1655                 out.getFD().sync();
1656             }
1657             assertThat(MediaStore.scanFile(getContentResolver(), mpegFile)).isNotNull();
1658             assertThat(getFileMimeTypeFromDatabase(mpegFile)).isEqualTo("audio/mpeg");
1659 
1660             // This rename changes MIME type from audio/mpeg to audio/basic
1661             assertCanRenameFile(mpegFile, nonMpegFile);
1662             assertThat(getFileMimeTypeFromDatabase(nonMpegFile)).isNotEqualTo("audio/mpeg");
1663 
1664             assertThat(MediaStore.scanFile(getContentResolver(), nonMpegFile)).isNotNull();
1665             // Above scan should read file metadata and update the MIME type to audio/mpeg
1666             assertThat(getFileMimeTypeFromDatabase(nonMpegFile)).isEqualTo("audio/mpeg");
1667         } finally {
1668             mpegFile.delete();
1669             nonMpegFile.delete();
1670         }
1671     }
1672 
1673     /**
1674      * Test that app without write permission for file can't update the file.
1675      */
1676     @Test
testRenameFileNotOwned()1677     public void testRenameFileNotOwned() throws Exception {
1678         final File videoFile1 = new File(getDcimDir(), VIDEO_FILE_NAME);
1679         final File videoFile2 = new File(getMoviesDir(), VIDEO_FILE_NAME);
1680         try {
1681             assertThat(createFileAs(APP_B_NO_PERMS, videoFile1.getAbsolutePath())).isTrue();
1682             // App can't rename a file owned by APP B.
1683             assertCantRenameFile(videoFile1, videoFile2);
1684 
1685             assertThat(videoFile2.createNewFile()).isTrue();
1686             // App can't rename a file to videoFile1 which is owned by APP B.
1687             assertCantRenameFile(videoFile2, videoFile1);
1688             // TODO(b/146346138): Test that app with right URI permission should be able to rename
1689             // the corresponding file
1690         } finally {
1691             deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile1.getAbsolutePath());
1692             videoFile2.delete();
1693         }
1694     }
1695 
1696     /**
1697      * Test that renaming directories is allowed and aligns to default directory restrictions.
1698      */
1699     @Test
testRenameDirectory()1700     public void testRenameDirectory() throws Exception {
1701         final File dcimDir = getDcimDir();
1702         final File downloadDir = getDownloadDir();
1703         final String nonMediaDirectoryName = TEST_DIRECTORY_NAME + "NonMedia";
1704         final File nonMediaDirectory = new File(downloadDir, nonMediaDirectoryName);
1705         final File pdfFile = new File(nonMediaDirectory, NONMEDIA_FILE_NAME);
1706 
1707         final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
1708         final File mediaDirectory1 = new File(dcimDir, mediaDirectoryName);
1709         final File videoFile1 = new File(mediaDirectory1, VIDEO_FILE_NAME);
1710         final File mediaDirectory2 = new File(downloadDir, mediaDirectoryName);
1711         final File videoFile2 = new File(mediaDirectory2, VIDEO_FILE_NAME);
1712         final File mediaDirectory3 = new File(getMoviesDir(), TEST_DIRECTORY_NAME);
1713         final File videoFile3 = new File(mediaDirectory3, VIDEO_FILE_NAME);
1714         final File mediaDirectory4 = new File(mediaDirectory3, mediaDirectoryName);
1715 
1716         try {
1717             if (!nonMediaDirectory.exists()) {
1718                 assertThat(nonMediaDirectory.mkdirs()).isTrue();
1719             }
1720             assertThat(pdfFile.createNewFile()).isTrue();
1721             // Move directory with pdf file to DCIM directory is not allowed.
1722             assertThat(nonMediaDirectory.renameTo(new File(dcimDir, nonMediaDirectoryName)))
1723                     .isFalse();
1724 
1725             if (!mediaDirectory1.exists()) {
1726                 assertThat(mediaDirectory1.mkdirs()).isTrue();
1727             }
1728             assertThat(videoFile1.createNewFile()).isTrue();
1729             // Renaming to and from default directories is not allowed.
1730             assertThat(mediaDirectory1.renameTo(dcimDir)).isFalse();
1731             // Moving top level default directories is not allowed.
1732             assertCantRenameDirectory(downloadDir, new File(dcimDir, TEST_DIRECTORY_NAME), null);
1733 
1734             // Moving media directory to Download directory is allowed.
1735             assertCanRenameDirectory(mediaDirectory1, mediaDirectory2, new File[] {videoFile1},
1736                     new File[] {videoFile2});
1737 
1738             // Moving media directory to Movies directory and renaming directory in new path is
1739             // allowed.
1740             assertCanRenameDirectory(mediaDirectory2, mediaDirectory3, new File[] {videoFile2},
1741                     new File[] {videoFile3});
1742 
1743             // Can't rename a mediaDirectory to non empty non Media directory.
1744             assertCantRenameDirectory(mediaDirectory3, nonMediaDirectory, new File[] {videoFile3});
1745             // Can't rename a file to a directory.
1746             assertCantRenameFile(videoFile3, mediaDirectory3);
1747             // Can't rename a directory to file.
1748             assertCantRenameDirectory(mediaDirectory3, pdfFile, null);
1749             if (!mediaDirectory4.exists()) {
1750                 assertThat(mediaDirectory4.mkdir()).isTrue();
1751             }
1752             // Can't rename a directory to subdirectory of itself.
1753             assertCantRenameDirectory(mediaDirectory3, mediaDirectory4, new File[] {videoFile3});
1754 
1755         } finally {
1756             pdfFile.delete();
1757             nonMediaDirectory.delete();
1758 
1759             videoFile1.delete();
1760             videoFile2.delete();
1761             videoFile3.delete();
1762             mediaDirectory1.delete();
1763             mediaDirectory2.delete();
1764             mediaDirectory3.delete();
1765             mediaDirectory4.delete();
1766         }
1767     }
1768 
1769     /**
1770      * Test that renaming directory checks file ownership permissions.
1771      */
1772     @Test
testRenameDirectoryNotOwned()1773     public void testRenameDirectoryNotOwned() throws Exception {
1774         final String mediaDirectoryName = TEST_DIRECTORY_NAME + "Media";
1775         File mediaDirectory1 = new File(getDcimDir(), mediaDirectoryName);
1776         File mediaDirectory2 = new File(getMoviesDir(), mediaDirectoryName);
1777         File videoFile = new File(mediaDirectory1, VIDEO_FILE_NAME);
1778 
1779         try {
1780             if (!mediaDirectory1.exists()) {
1781                 assertThat(mediaDirectory1.mkdirs()).isTrue();
1782             }
1783             assertThat(createFileAs(APP_B_NO_PERMS, videoFile.getAbsolutePath())).isTrue();
1784             // App doesn't have access to videoFile1, can't rename mediaDirectory1.
1785             assertThat(mediaDirectory1.renameTo(mediaDirectory2)).isFalse();
1786             assertThat(videoFile.exists()).isTrue();
1787             // Test app can delete the file since the file is not moved to new directory.
1788             assertThat(deleteFileAs(APP_B_NO_PERMS, videoFile.getAbsolutePath())).isTrue();
1789         } finally {
1790             deleteFileAsNoThrow(APP_B_NO_PERMS, videoFile.getAbsolutePath());
1791             mediaDirectory1.delete();
1792         }
1793     }
1794 
1795     /**
1796      * Test renaming empty directory is allowed
1797      */
1798     @Test
testRenameEmptyDirectory()1799     public void testRenameEmptyDirectory() throws Exception {
1800         final String emptyDirectoryName = TEST_DIRECTORY_NAME + "Media";
1801         File emptyDirectoryOldPath = new File(getDcimDir(), emptyDirectoryName);
1802         File emptyDirectoryNewPath = new File(getMoviesDir(), TEST_DIRECTORY_NAME + "23456");
1803         try {
1804             if (emptyDirectoryOldPath.exists()) {
1805                 executeShellCommand("rm -r " + emptyDirectoryOldPath.getPath());
1806             }
1807             assertThat(emptyDirectoryOldPath.mkdirs()).isTrue();
1808             assertCanRenameDirectory(emptyDirectoryOldPath, emptyDirectoryNewPath, null, null);
1809         } finally {
1810             emptyDirectoryOldPath.delete();
1811             emptyDirectoryNewPath.delete();
1812         }
1813     }
1814 
1815     /**
1816      * Test that apps can create and delete hidden file.
1817      */
1818     @Test
testCanCreateHiddenFile()1819     public void testCanCreateHiddenFile() throws Exception {
1820         final File hiddenImageFile = new File(getDownloadDir(), ".hiddenFile" + IMAGE_FILE_NAME);
1821         try {
1822             assertThat(hiddenImageFile.createNewFile()).isTrue();
1823             // Write to hidden file is allowed.
1824             try (FileOutputStream fos = new FileOutputStream(hiddenImageFile)) {
1825                 fos.write(BYTES_DATA1);
1826             }
1827             assertFileContent(hiddenImageFile, BYTES_DATA1);
1828 
1829             assertNotMediaTypeImage(hiddenImageFile);
1830 
1831             assertDirectoryContains(getDownloadDir(), hiddenImageFile);
1832             assertThat(getFileRowIdFromDatabase(hiddenImageFile)).isNotEqualTo(-1);
1833 
1834             // We can delete hidden file
1835             assertThat(hiddenImageFile.delete()).isTrue();
1836             assertThat(hiddenImageFile.exists()).isFalse();
1837         } finally {
1838             hiddenImageFile.delete();
1839         }
1840     }
1841 
1842     /**
1843      * Test that FUSE upper-fs is consistent with lower-fs after the lower-fs fd is closed.
1844      */
1845     @Test
testInodeStatConsistency()1846     public void testInodeStatConsistency() throws Exception {
1847         File file = new File(getDcimDir(), IMAGE_FILE_NAME);
1848 
1849         try {
1850             byte[] writeBuffer = new byte[10];
1851             Arrays.fill(writeBuffer, (byte) 1);
1852 
1853             assertThat(file.createNewFile()).isTrue();
1854             // Scanning a file is essential as files created via filepath will be marked
1855             // as isPending, and we do not set listener for pending files as it can lead to
1856             // performance overhead. See: I34611f0ee897dc676e7653beb7943aa6de58c55a.
1857             MediaStore.scanFile(getContentResolver(), file);
1858 
1859             // File operation #1 (to lower-fs)
1860             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
1861 
1862             // File operation #2 (to fuse). This caches the inode for the file.
1863             file.exists();
1864 
1865             // Write bytes directly to lower-fs
1866             Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
1867 
1868             // Close should invalidate inode cache for this file.
1869             writePfd.close();
1870             Thread.sleep(1000);
1871 
1872             long fuseFileSize = file.length();
1873             assertThat(writeBuffer.length).isEqualTo(fuseFileSize);
1874         } finally {
1875             file.delete();
1876         }
1877     }
1878 
1879     /**
1880      * Test that apps can rename a hidden file.
1881      */
1882     @Test
testCanRenameHiddenFile()1883     public void testCanRenameHiddenFile() throws Exception {
1884         final String hiddenFileName = ".hidden" + IMAGE_FILE_NAME;
1885         final File hiddenImageFile1 = new File(getDcimDir(), hiddenFileName);
1886         final File hiddenImageFile2 = new File(getDownloadDir(), hiddenFileName);
1887         final File imageFile = new File(getDownloadDir(), IMAGE_FILE_NAME);
1888         try {
1889             assertThat(hiddenImageFile1.createNewFile()).isTrue();
1890             assertCanRenameFile(hiddenImageFile1, hiddenImageFile2);
1891             assertNotMediaTypeImage(hiddenImageFile2);
1892 
1893             // We can also rename hidden file to non-hidden
1894             assertCanRenameFile(hiddenImageFile2, imageFile);
1895             assertIsMediaTypeImage(imageFile);
1896 
1897             // We can rename non-hidden file to hidden
1898             assertCanRenameFile(imageFile, hiddenImageFile1);
1899             assertNotMediaTypeImage(hiddenImageFile1);
1900         } finally {
1901             hiddenImageFile1.delete();
1902             hiddenImageFile2.delete();
1903             imageFile.delete();
1904         }
1905     }
1906 
1907     /**
1908      * Test that files in hidden directory have MEDIA_TYPE=MEDIA_TYPE_NONE
1909      */
1910     @Test
testHiddenDirectory()1911     public void testHiddenDirectory() throws Exception {
1912         final File hiddenDir = new File(getDownloadDir(), ".hidden" + TEST_DIRECTORY_NAME);
1913         final File hiddenImageFile = new File(hiddenDir, IMAGE_FILE_NAME);
1914         final File nonHiddenDir = new File(getDownloadDir(), TEST_DIRECTORY_NAME);
1915         final File imageFile = new File(nonHiddenDir, IMAGE_FILE_NAME);
1916         try {
1917             if (!hiddenDir.exists()) {
1918                 assertThat(hiddenDir.mkdir()).isTrue();
1919             }
1920             assertThat(hiddenImageFile.createNewFile()).isTrue();
1921 
1922             assertNotMediaTypeImage(hiddenImageFile);
1923 
1924             // Renaming hiddenDir to nonHiddenDir makes the imageFile non-hidden and vice versa
1925             assertCanRenameDirectory(
1926                     hiddenDir, nonHiddenDir, new File[] {hiddenImageFile}, new File[] {imageFile});
1927             assertIsMediaTypeImage(imageFile);
1928 
1929             assertCanRenameDirectory(
1930                     nonHiddenDir, hiddenDir, new File[] {imageFile}, new File[] {hiddenImageFile});
1931             assertNotMediaTypeImage(hiddenImageFile);
1932         } finally {
1933             hiddenImageFile.delete();
1934             imageFile.delete();
1935             hiddenDir.delete();
1936             nonHiddenDir.delete();
1937         }
1938     }
1939 
1940     /**
1941      * Test that files in directory with nomedia have MEDIA_TYPE=MEDIA_TYPE_NONE
1942      */
1943     @Test
testHiddenDirectory_nomedia()1944     public void testHiddenDirectory_nomedia() throws Exception {
1945         final File directoryNoMedia = new File(getDownloadDir(), "nomedia" + TEST_DIRECTORY_NAME);
1946         final File noMediaFile = new File(directoryNoMedia, ".nomedia");
1947         final File imageFile = new File(directoryNoMedia, IMAGE_FILE_NAME);
1948         final File videoFile = new File(directoryNoMedia, VIDEO_FILE_NAME);
1949         try {
1950             if (!directoryNoMedia.exists()) {
1951                 assertThat(directoryNoMedia.mkdir()).isTrue();
1952             }
1953             assertThat(noMediaFile.createNewFile()).isTrue();
1954             assertThat(imageFile.createNewFile()).isTrue();
1955 
1956             assertNotMediaTypeImage(imageFile);
1957 
1958             // Deleting the .nomedia file makes the parent directory non hidden.
1959             noMediaFile.delete();
1960             MediaStore.scanFile(getContentResolver(), directoryNoMedia);
1961             assertIsMediaTypeImage(imageFile);
1962 
1963             // Creating the .nomedia file makes the parent directory hidden again
1964             assertThat(noMediaFile.createNewFile()).isTrue();
1965             MediaStore.scanFile(getContentResolver(), directoryNoMedia);
1966             assertNotMediaTypeImage(imageFile);
1967 
1968             // Renaming the .nomedia file to non hidden file makes the parent directory non hidden.
1969             assertCanRenameFile(noMediaFile, videoFile);
1970             assertIsMediaTypeImage(imageFile);
1971         } finally {
1972             noMediaFile.delete();
1973             imageFile.delete();
1974             videoFile.delete();
1975             directoryNoMedia.delete();
1976         }
1977     }
1978 
1979     /**
1980      * Test that only file manager and app that created the hidden file can list it.
1981      */
1982     @Test
testListHiddenFile()1983     public void testListHiddenFile() throws Exception {
1984         final File dcimDir = getDcimDir();
1985         final String hiddenImageFileName = ".hidden" + IMAGE_FILE_NAME;
1986         final File hiddenImageFile = new File(dcimDir, hiddenImageFileName);
1987         try {
1988             assertThat(hiddenImageFile.createNewFile()).isTrue();
1989             assertNotMediaTypeImage(hiddenImageFile);
1990 
1991             assertDirectoryContains(dcimDir, hiddenImageFile);
1992 
1993             // TestApp with read permissions can't see the hidden image file created by other app
1994             assertThat(listAs(APP_A_HAS_RES, dcimDir.getAbsolutePath()))
1995                     .doesNotContain(hiddenImageFileName);
1996 
1997             // But file manager can
1998             assertThat(listAs(APP_FM, dcimDir.getAbsolutePath()))
1999                     .contains(hiddenImageFileName);
2000 
2001             // Gallery cannot see the hidden image file created by other app
2002             final int resAppUid =
2003                     getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(),
2004                             0);
2005             try {
2006                 allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2007                 assertThat(listAs(APP_A_HAS_RES, dcimDir.getAbsolutePath()))
2008                         .doesNotContain(hiddenImageFileName);
2009             } finally {
2010                 denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2011             }
2012         } finally {
2013             hiddenImageFile.delete();
2014         }
2015     }
2016 
2017     @Test
testOpenPendingAndTrashed()2018     public void testOpenPendingAndTrashed() throws Exception {
2019         final File pendingImageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
2020         final File trashedVideoFile = new File(getPicturesDir(), VIDEO_FILE_NAME);
2021         final File pendingPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
2022         final File trashedPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2023         Uri pendingImgaeFileUri = null;
2024         Uri trashedVideoFileUri = null;
2025         Uri pendingPdfFileUri = null;
2026         Uri trashedPdfFileUri = null;
2027         try {
2028             pendingImgaeFileUri = createPendingFile(pendingImageFile);
2029             assertOpenPendingOrTrashed(pendingImgaeFileUri, /*isImageOrVideo*/ true);
2030 
2031             pendingPdfFileUri = createPendingFile(pendingPdfFile);
2032             assertOpenPendingOrTrashed(pendingPdfFileUri, /*isImageOrVideo*/ false);
2033 
2034             trashedVideoFileUri = createTrashedFile(trashedVideoFile);
2035             assertOpenPendingOrTrashed(trashedVideoFileUri, /*isImageOrVideo*/ true);
2036 
2037             trashedPdfFileUri = createTrashedFile(trashedPdfFile);
2038             assertOpenPendingOrTrashed(trashedPdfFileUri, /*isImageOrVideo*/ false);
2039 
2040         } finally {
2041             deleteFiles(pendingImageFile, pendingImageFile, trashedVideoFile,
2042                     trashedPdfFile);
2043             deleteWithMediaProviderNoThrow(pendingImgaeFileUri, trashedVideoFileUri,
2044                     pendingPdfFileUri, trashedPdfFileUri);
2045         }
2046     }
2047 
2048     @Test
testListPendingAndTrashed()2049     public void testListPendingAndTrashed() throws Exception {
2050         final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
2051         final File pdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2052         Uri imageFileUri = null;
2053         Uri pdfFileUri = null;
2054         try {
2055             imageFileUri = createPendingFile(imageFile);
2056             // Check that only owner package, file manager and system gallery can list pending image
2057             // file.
2058             assertListPendingOrTrashed(imageFileUri, imageFile, /*isImageOrVideo*/ true);
2059 
2060             trashFile(imageFileUri);
2061             // Check that only owner package, file manager and system gallery can list trashed image
2062             // file.
2063             assertListPendingOrTrashed(imageFileUri, imageFile, /*isImageOrVideo*/ true);
2064 
2065             pdfFileUri = createPendingFile(pdfFile);
2066             // Check that only owner package, file manager can list pending non media file.
2067             assertListPendingOrTrashed(pdfFileUri, pdfFile, /*isImageOrVideo*/ false);
2068 
2069             trashFile(pdfFileUri);
2070             // Check that only owner package, file manager can list trashed non media file.
2071             assertListPendingOrTrashed(pdfFileUri, pdfFile, /*isImageOrVideo*/ false);
2072         } finally {
2073             deleteWithMediaProviderNoThrow(imageFileUri, pdfFileUri);
2074             deleteFiles(imageFile, pdfFile);
2075         }
2076     }
2077 
2078     @Test
testDeletePendingAndTrashed_ownerCanDelete()2079     public void testDeletePendingAndTrashed_ownerCanDelete() throws Exception {
2080         final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
2081         final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
2082         final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2083         final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
2084         // Actual path of the file gets rewritten for pending and trashed files.
2085         String pendingVideoFilePath = null;
2086         String trashedImageFilePath = null;
2087         String pendingPdfFilePath = null;
2088         String trashedPdfFilePath = null;
2089         try {
2090             pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
2091             trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
2092             pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
2093             trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
2094 
2095             // App can delete its own pending and trashed file.
2096             assertCanDeletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
2097                     trashedPdfFilePath);
2098         } finally {
2099             deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
2100                     trashedPdfFilePath);
2101             deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
2102         }
2103     }
2104 
2105     @Test
testDeletePendingAndTrashed_otherAppCantDelete()2106     public void testDeletePendingAndTrashed_otherAppCantDelete() throws Exception {
2107         final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
2108         final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
2109         final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2110         final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
2111         // Actual path of the file gets rewritten for pending and trashed files.
2112         String pendingVideoFilePath = null;
2113         String trashedImageFilePath = null;
2114         String pendingPdfFilePath = null;
2115         String trashedPdfFilePath = null;
2116         try {
2117             pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
2118             trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
2119             pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
2120             trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
2121 
2122             // App can't delete other app's pending and trashed file.
2123             assertCantDeletePathsAs(APP_A_HAS_RES, pendingVideoFilePath, trashedImageFilePath,
2124                     pendingPdfFilePath, trashedPdfFilePath);
2125         } finally {
2126             deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
2127                     trashedPdfFilePath);
2128             deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
2129         }
2130     }
2131 
2132     @Test
testDeletePendingAndTrashed_fileManagerCanDelete()2133     public void testDeletePendingAndTrashed_fileManagerCanDelete() throws Exception {
2134         final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
2135         final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
2136         final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2137         final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
2138         // Actual path of the file gets rewritten for pending and trashed files.
2139         String pendingVideoFilePath = null;
2140         String trashedImageFilePath = null;
2141         String pendingPdfFilePath = null;
2142         String trashedPdfFilePath = null;
2143         try {
2144             pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
2145             trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
2146             pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
2147             trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
2148 
2149             // File Manager can delete any pending and trashed file
2150             assertCanDeletePathsAs(APP_FM, pendingVideoFilePath, trashedImageFilePath,
2151                     pendingPdfFilePath, trashedPdfFilePath);
2152         } finally {
2153             deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
2154                     trashedPdfFilePath);
2155             deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
2156         }
2157     }
2158 
2159     @Test
testDeletePendingAndTrashed_systemGalleryCanDeleteMedia()2160     public void testDeletePendingAndTrashed_systemGalleryCanDeleteMedia() throws Exception {
2161         final File pendingVideoFile = new File(getDcimDir(), VIDEO_FILE_NAME);
2162         final File trashedImageFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
2163         final File pendingPdfFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2164         final File trashedPdfFile = new File(getDocumentsDir(), NONMEDIA_FILE_NAME);
2165         // Actual path of the file gets rewritten for pending and trashed files.
2166         String pendingVideoFilePath = null;
2167         String trashedImageFilePath = null;
2168         String pendingPdfFilePath = null;
2169         String trashedPdfFilePath = null;
2170         try {
2171             pendingVideoFilePath = getFilePathFromUri(createPendingFile(pendingVideoFile));
2172             trashedImageFilePath = getFilePathFromUri(createTrashedFile(trashedImageFile));
2173             pendingPdfFilePath = getFilePathFromUri(createPendingFile(pendingPdfFile));
2174             trashedPdfFilePath = getFilePathFromUri(createTrashedFile(trashedPdfFile));
2175 
2176             // System Gallery can delete any pending and trashed image or video file.
2177             final int resAppUid =
2178                     getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(),
2179                             0);
2180             try {
2181                 allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2182                 assertTrue(isMediaTypeImageOrVideo(new File(pendingVideoFilePath)));
2183                 assertTrue(isMediaTypeImageOrVideo(new File(trashedImageFilePath)));
2184                 assertCanDeletePathsAs(APP_A_HAS_RES, pendingVideoFilePath, trashedImageFilePath);
2185 
2186                 // System Gallery can't delete other app's pending and trashed pdf file.
2187                 assertFalse(isMediaTypeImageOrVideo(new File(pendingPdfFilePath)));
2188                 assertFalse(isMediaTypeImageOrVideo(new File(trashedPdfFilePath)));
2189                 assertCantDeletePathsAs(APP_A_HAS_RES, pendingPdfFilePath, trashedPdfFilePath);
2190             } finally {
2191                 denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2192             }
2193         } finally {
2194             deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
2195                     trashedPdfFilePath);
2196             deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
2197         }
2198     }
2199 
2200     @Test
testQueryOtherAppsFiles()2201     public void testQueryOtherAppsFiles() throws Exception {
2202         final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
2203         final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
2204         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
2205         final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
2206         try {
2207             // Apps can't query other app's pending file, hence create file and publish it.
2208             assertCreatePublishedFilesAs(
2209                     APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
2210 
2211             // Since the test doesn't have READ_EXTERNAL_STORAGE nor any other special permissions,
2212             // it can't query for another app's contents.
2213             assertCantQueryFile(otherAppImg);
2214             assertCantQueryFile(otherAppMusic);
2215             assertCantQueryFile(otherAppPdf);
2216             assertCantQueryFile(otherHiddenFile);
2217         } finally {
2218             deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
2219         }
2220     }
2221 
2222     @Test
testSystemGalleryQueryOtherAppsFiles()2223     public void testSystemGalleryQueryOtherAppsFiles() throws Exception {
2224         final File otherAppPdf = new File(getDownloadDir(), "other" + NONMEDIA_FILE_NAME);
2225         final File otherAppImg = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
2226         final File otherAppMusic = new File(getMusicDir(), "other" + AUDIO_FILE_NAME);
2227         final File otherHiddenFile = new File(getPicturesDir(), ".otherHiddenFile.jpg");
2228         try {
2229             // Apps can't query other app's pending file, hence create file and publish it.
2230             assertCreatePublishedFilesAs(
2231                     APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
2232 
2233             // System gallery apps have access to video and image files
2234             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
2235 
2236             assertCanQueryAndOpenFile(otherAppImg, "rw");
2237             // System gallery doesn't have access to hidden image files of other app
2238             assertCantQueryFile(otherHiddenFile);
2239             // But no access to PDFs or music files
2240             assertCantQueryFile(otherAppMusic);
2241             assertCantQueryFile(otherAppPdf);
2242         } finally {
2243             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
2244             deleteFilesAs(APP_B_NO_PERMS, otherAppImg, otherAppMusic, otherAppPdf, otherHiddenFile);
2245         }
2246     }
2247 
2248     /**
2249      * Test that System Gallery app can rename any directory under the default directories
2250      * designated for images and videos, even if they contain other apps' contents that
2251      * System Gallery doesn't have read access to.
2252      */
2253     @Test
testSystemGalleryCanRenameImageAndVideoDirs()2254     public void testSystemGalleryCanRenameImageAndVideoDirs() throws Exception {
2255         final File dirInDcim = new File(getDcimDir(), TEST_DIRECTORY_NAME);
2256         final File dirInPictures = new File(getPicturesDir(), TEST_DIRECTORY_NAME);
2257         final File dirInPodcasts = new File(getPodcastsDir(), TEST_DIRECTORY_NAME);
2258         final File otherAppImageFile1 = new File(dirInDcim, "other_" + IMAGE_FILE_NAME);
2259         final File otherAppVideoFile1 = new File(dirInDcim, "other_" + VIDEO_FILE_NAME);
2260         final File otherAppPdfFile1 = new File(dirInDcim, "other_" + NONMEDIA_FILE_NAME);
2261         final File otherAppImageFile2 = new File(dirInPictures, "other_" + IMAGE_FILE_NAME);
2262         final File otherAppVideoFile2 = new File(dirInPictures, "other_" + VIDEO_FILE_NAME);
2263         final File otherAppPdfFile2 = new File(dirInPictures, "other_" + NONMEDIA_FILE_NAME);
2264         try {
2265             assertThat(dirInDcim.exists() || dirInDcim.mkdir()).isTrue();
2266 
2267             executeShellCommand("touch " + otherAppPdfFile1);
2268             MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
2269 
2270             allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
2271 
2272             assertCreateFilesAs(APP_A_HAS_RES, otherAppImageFile1, otherAppVideoFile1);
2273 
2274             // System gallery privileges don't go beyond DCIM, Movies and Pictures boundaries.
2275             assertCantRenameDirectory(dirInDcim, dirInPodcasts, /*oldFilesList*/ null);
2276 
2277             // Rename should succeed, but System Gallery still can't access that PDF file!
2278             assertCanRenameDirectory(dirInDcim, dirInPictures,
2279                     new File[] {otherAppImageFile1, otherAppVideoFile1},
2280                     new File[] {otherAppImageFile2, otherAppVideoFile2});
2281             assertThat(getFileRowIdFromDatabase(otherAppPdfFile1)).isEqualTo(-1);
2282             assertThat(getFileRowIdFromDatabase(otherAppPdfFile2)).isEqualTo(-1);
2283         } finally {
2284             executeShellCommand("rm " + otherAppPdfFile1);
2285             executeShellCommand("rm " + otherAppPdfFile2);
2286             MediaStore.scanFile(getContentResolver(), otherAppPdfFile1);
2287             MediaStore.scanFile(getContentResolver(), otherAppPdfFile2);
2288             otherAppImageFile1.delete();
2289             otherAppImageFile2.delete();
2290             otherAppVideoFile1.delete();
2291             otherAppVideoFile2.delete();
2292             otherAppPdfFile1.delete();
2293             otherAppPdfFile2.delete();
2294             dirInDcim.delete();
2295             dirInPictures.delete();
2296             denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
2297         }
2298     }
2299 
2300     /**
2301      * Test that row ID corresponding to deleted path is restored on subsequent create.
2302      */
2303     @Test
testCreateCanRestoreDeletedRowId()2304     public void testCreateCanRestoreDeletedRowId() throws Exception {
2305         final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
2306         final ContentResolver cr = getContentResolver();
2307 
2308         try {
2309             assertThat(imageFile.createNewFile()).isTrue();
2310             final long oldRowId = getFileRowIdFromDatabase(imageFile);
2311             assertThat(oldRowId).isNotEqualTo(-1);
2312             final Uri uriOfOldFile = MediaStore.scanFile(cr, imageFile);
2313             assertThat(uriOfOldFile).isNotNull();
2314 
2315             assertThat(imageFile.delete()).isTrue();
2316             // We should restore old row Id corresponding to deleted imageFile.
2317             assertThat(imageFile.createNewFile()).isTrue();
2318             assertThat(getFileRowIdFromDatabase(imageFile)).isEqualTo(oldRowId);
2319             assertThat(cr.openFileDescriptor(uriOfOldFile, "rw")).isNotNull();
2320 
2321             assertThat(imageFile.delete()).isTrue();
2322             assertThat(createFileAs(APP_B_NO_PERMS, imageFile.getAbsolutePath())).isTrue();
2323 
2324             final Uri uriOfNewFile = MediaStore.scanFile(getContentResolver(), imageFile);
2325             assertThat(uriOfNewFile).isNotNull();
2326             // We shouldn't restore deleted row Id if delete & create are called from different apps
2327             assertThat(Integer.getInteger(uriOfNewFile.getLastPathSegment()))
2328                     .isNotEqualTo(oldRowId);
2329         } finally {
2330             imageFile.delete();
2331             deleteFileAsNoThrow(APP_B_NO_PERMS, imageFile.getAbsolutePath());
2332         }
2333     }
2334 
2335     /**
2336      * Test that row ID corresponding to deleted path is restored on subsequent rename.
2337      */
2338     @Test
testRenameCanRestoreDeletedRowId()2339     public void testRenameCanRestoreDeletedRowId() throws Exception {
2340         final File imageFile = new File(getDcimDir(), IMAGE_FILE_NAME);
2341         final File temporaryFile = new File(getDownloadDir(), IMAGE_FILE_NAME + "_.tmp");
2342         final ContentResolver cr = getContentResolver();
2343 
2344         try {
2345             assertThat(imageFile.createNewFile()).isTrue();
2346             final Uri oldUri = MediaStore.scanFile(cr, imageFile);
2347             assertThat(oldUri).isNotNull();
2348 
2349             Files.copy(imageFile, temporaryFile);
2350             assertThat(imageFile.delete()).isTrue();
2351             assertCanRenameFile(temporaryFile, imageFile);
2352 
2353             final Uri newUri = MediaStore.scanFile(cr, imageFile);
2354             assertThat(newUri).isNotNull();
2355             assertThat(newUri.getLastPathSegment()).isEqualTo(oldUri.getLastPathSegment());
2356             // oldUri of imageFile is still accessible after delete and rename.
2357             assertThat(cr.openFileDescriptor(oldUri, "rw")).isNotNull();
2358         } finally {
2359             imageFile.delete();
2360             temporaryFile.delete();
2361         }
2362     }
2363 
2364     @Test
testCantCreateOrRenameFileWithInvalidName()2365     public void testCantCreateOrRenameFileWithInvalidName() throws Exception {
2366         File invalidFile = new File(getDownloadDir(), "<>");
2367         File validFile = new File(getDownloadDir(), NONMEDIA_FILE_NAME);
2368         try {
2369             assertThrows(IOException.class, "Operation not permitted",
2370                     () -> {
2371                         invalidFile.createNewFile();
2372                     });
2373 
2374             assertThat(validFile.createNewFile()).isTrue();
2375             // We can't rename a file to a file name with invalid FAT characters.
2376             assertCantRenameFile(validFile, invalidFile);
2377         } finally {
2378             invalidFile.delete();
2379             validFile.delete();
2380         }
2381     }
2382 
2383     @Test
testRenameWithSpecialChars()2384     public void testRenameWithSpecialChars() throws Exception {
2385         final String specialCharsSuffix = "'`~!@#$%^& ()_+-={}[];'.)";
2386 
2387         final File fileSpecialChars =
2388                 new File(getDownloadDir(), NONMEDIA_FILE_NAME + specialCharsSuffix);
2389 
2390         final File dirSpecialChars =
2391                 new File(getDownloadDir(), TEST_DIRECTORY_NAME + specialCharsSuffix);
2392         final File file1 = new File(dirSpecialChars, NONMEDIA_FILE_NAME);
2393         final File fileSpecialChars1 =
2394                 new File(dirSpecialChars, NONMEDIA_FILE_NAME + specialCharsSuffix);
2395 
2396         final File renamedDir = new File(getDocumentsDir(), TEST_DIRECTORY_NAME);
2397         final File file2 = new File(renamedDir, NONMEDIA_FILE_NAME);
2398         final File fileSpecialChars2 =
2399                 new File(renamedDir, NONMEDIA_FILE_NAME + specialCharsSuffix);
2400         try {
2401             assertTrue(fileSpecialChars.createNewFile());
2402             if (!dirSpecialChars.exists()) {
2403                 assertTrue(dirSpecialChars.mkdir());
2404             }
2405             assertTrue(file1.createNewFile());
2406 
2407             // We can rename file name with special characters
2408             assertCanRenameFile(fileSpecialChars, fileSpecialChars1);
2409 
2410             // We can rename directory name with special characters
2411             assertCanRenameDirectory(dirSpecialChars, renamedDir,
2412                     new File[] {file1, fileSpecialChars1}, new File[] {file2, fileSpecialChars2});
2413         } finally {
2414             file1.delete();
2415             file2.delete();
2416             fileSpecialChars.delete();
2417             fileSpecialChars1.delete();
2418             fileSpecialChars2.delete();
2419             dirSpecialChars.delete();
2420             renamedDir.delete();
2421         }
2422     }
2423 
2424     /**
2425      * Test that IS_PENDING is set for files created via filepath
2426      */
2427     @Test
testPendingFromFuse()2428     public void testPendingFromFuse() throws Exception {
2429         final File pendingFile = new File(getDcimDir(), IMAGE_FILE_NAME);
2430         final File otherPendingFile = new File(getDcimDir(), VIDEO_FILE_NAME);
2431         try {
2432             assertTrue(pendingFile.createNewFile());
2433             // Newly created file should have IS_PENDING set
2434             try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
2435                 assertTrue(c.moveToFirst());
2436                 assertThat(c.getInt(0)).isEqualTo(1);
2437             }
2438 
2439             // If we query with MATCH_EXCLUDE, we should still see this pendingFile
2440             try (Cursor c = queryFileExcludingPending(pendingFile,
2441                     MediaStore.MediaColumns.IS_PENDING)) {
2442                 assertThat(c.getCount()).isEqualTo(1);
2443                 assertTrue(c.moveToFirst());
2444                 assertThat(c.getInt(0)).isEqualTo(1);
2445             }
2446 
2447             assertNotNull(MediaStore.scanFile(getContentResolver(), pendingFile));
2448 
2449             // IS_PENDING should be unset after the scan
2450             try (Cursor c = queryFile(pendingFile, MediaStore.MediaColumns.IS_PENDING)) {
2451                 assertTrue(c.moveToFirst());
2452                 assertThat(c.getInt(0)).isEqualTo(0);
2453             }
2454 
2455             assertCreateFilesAs(APP_A_HAS_RES, otherPendingFile);
2456             // We can't query other apps pending file from FUSE with MATCH_EXCLUDE
2457             try (Cursor c = queryFileExcludingPending(otherPendingFile,
2458                     MediaStore.MediaColumns.IS_PENDING)) {
2459                 assertThat(c.getCount()).isEqualTo(0);
2460             }
2461         } finally {
2462             pendingFile.delete();
2463             deleteFileAsNoThrow(APP_A_HAS_RES, otherPendingFile.getAbsolutePath());
2464         }
2465     }
2466 
2467     /**
2468      * Test that we don't allow renaming to top level directory
2469      */
2470     @Test
testCantRenameToTopLevelDirectory()2471     public void testCantRenameToTopLevelDirectory() throws Exception {
2472         final File topLevelDir1 = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME + "_1");
2473         final File topLevelDir2 = new File(getExternalStorageDir(), TEST_DIRECTORY_NAME + "_2");
2474         final File nonTopLevelDir = new File(getDcimDir(), TEST_DIRECTORY_NAME);
2475         try {
2476             createDirectoryAsLegacyApp(topLevelDir1);
2477             assertTrue(topLevelDir1.exists());
2478 
2479             // We can't rename a top level directory to a top level directory
2480             assertCantRenameDirectory(topLevelDir1, topLevelDir2, null);
2481 
2482             // However, we can rename a top level directory to non-top level directory.
2483             assertCanRenameDirectory(topLevelDir1, nonTopLevelDir, null, null);
2484 
2485             // We can't rename a non-top level directory to a top level directory.
2486             assertCantRenameDirectory(nonTopLevelDir, topLevelDir2, null);
2487         } finally {
2488             deleteAsLegacyApp(topLevelDir1);
2489             deleteAsLegacyApp(topLevelDir2);
2490             nonTopLevelDir.delete();
2491         }
2492     }
2493 
2494     @Test
testCanCreateDefaultDirectory()2495     public void testCanCreateDefaultDirectory() throws Exception {
2496         final File podcastsDir = getPodcastsDir();
2497         try {
2498             if (podcastsDir.exists()) {
2499                 deleteAsLegacyApp(podcastsDir);
2500             }
2501             assertThat(podcastsDir.mkdir()).isTrue();
2502         } finally {
2503             createDirectoryAsLegacyApp(podcastsDir);
2504         }
2505     }
2506 
2507     /**
2508      * b/168830497: Test that app can write to file in DCIM/Camera even with .nomedia presence
2509      */
2510     @Test
testCanWriteToDCIMCameraWithNomedia()2511     public void testCanWriteToDCIMCameraWithNomedia() throws Exception {
2512         final File cameraDir = new File(getDcimDir(), "Camera");
2513         final File nomediaFile = new File(cameraDir, ".nomedia");
2514         Uri targetUri = null;
2515 
2516         try {
2517             // Recreate required file and directory
2518             if (cameraDir.exists()) {
2519                 // This is a work around to address a known inode cache inconsistency issue
2520                 // that occurs when test runs for the second time.
2521                 deleteAsLegacyApp(cameraDir);
2522             }
2523 
2524             createDirectoryAsLegacyApp(cameraDir);
2525             assertTrue(cameraDir.exists());
2526 
2527             createFileAsLegacyApp(nomediaFile);
2528             assertTrue(nomediaFile.exists());
2529 
2530             ContentValues values = new ContentValues();
2531             values.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Camera");
2532             targetUri = getContentResolver().insert(getImageContentUri(), values, Bundle.EMPTY);
2533             assertNotNull(targetUri);
2534 
2535             try (ParcelFileDescriptor pfd =
2536                          getContentResolver().openFileDescriptor(targetUri, "w")) {
2537                 assertThat(pfd).isNotNull();
2538                 Os.write(pfd.getFileDescriptor(), ByteBuffer.wrap(BYTES_DATA1));
2539             }
2540 
2541             assertFileContent(new File(getFilePathFromUri(targetUri)), BYTES_DATA1);
2542         } finally {
2543             deleteWithMediaProviderNoThrow(targetUri);
2544             deleteAsLegacyApp(nomediaFile);
2545             deleteAsLegacyApp(cameraDir);
2546         }
2547     }
2548 
2549     /**
2550      * b/182479650: Test that Screenshots directory is not hidden because of .nomedia presence
2551      */
2552     @Test
testNoMediaDoesntHideSpecialDirectories()2553     public void testNoMediaDoesntHideSpecialDirectories() throws Exception {
2554         for (File directory : new File [] {
2555                 getDcimDir(),
2556                 getDownloadDir(),
2557                 new File(getDcimDir(), "Camera"),
2558                 new File(getPicturesDir(), Environment.DIRECTORY_SCREENSHOTS),
2559                 new File(getMoviesDir(), Environment.DIRECTORY_SCREENSHOTS),
2560                 new File(getExternalStorageDir(), Environment.DIRECTORY_SCREENSHOTS)
2561         }) {
2562             assertNoMediaDoesntHideSpecialDirectories(directory);
2563         }
2564     }
2565 
assertNoMediaDoesntHideSpecialDirectories(File directory)2566     private void assertNoMediaDoesntHideSpecialDirectories(File directory) throws Exception {
2567         final File nomediaFile = new File(directory, ".nomedia");
2568         final File videoFile = new File(directory, VIDEO_FILE_NAME);
2569         Log.d(TAG, "Directory " + directory);
2570 
2571         try {
2572             // Recreate required file and directory
2573             if (!directory.exists()) {
2574                 Log.d(TAG, "mkdir directory " + directory);
2575                 createDirectoryAsLegacyApp(directory);
2576             }
2577             assertWithMessage("Exists " + directory).that(directory.exists()).isTrue();
2578 
2579             Log.d(TAG, "CreateFileAs " + nomediaFile);
2580             createFileAsLegacyApp(nomediaFile);
2581             assertWithMessage("Exists " + nomediaFile).that(nomediaFile.exists()).isTrue();
2582 
2583             createFileAsLegacyApp(videoFile);
2584             assertWithMessage("Exists " + videoFile).that(videoFile.exists()).isTrue();
2585             final Uri targetUri = MediaStore.scanFile(getContentResolver(), videoFile);
2586             assertWithMessage("Scan result for " + videoFile).that(targetUri)
2587                     .isNotNull();
2588 
2589             assertWithMessage("Uri path segment for " + targetUri)
2590                     .that(targetUri.getPathSegments()).contains("video");
2591 
2592             // Verify that the imageFile is not hidden because of .nomedia presence
2593             assertWithMessage("Query as other app ")
2594                     .that(canQueryOnUri(APP_A_HAS_RES, targetUri)).isTrue();
2595         } finally {
2596             deleteAsLegacyApp(videoFile);
2597             deleteAsLegacyApp(nomediaFile);
2598             deleteAsLegacyApp(directory);
2599         }
2600     }
2601 
2602     /**
2603      * Test that readdir lists unsupported file types in default directories.
2604      */
2605     @Test
testListUnsupportedFileType()2606     public void testListUnsupportedFileType() throws Exception {
2607         final File pdfFile = new File(getDcimDir(), NONMEDIA_FILE_NAME);
2608         final File videoFile = new File(getMusicDir(), VIDEO_FILE_NAME);
2609         try {
2610             // TEST_APP_A with storage permission should not see pdf file in DCIM
2611             createFileAsLegacyApp(pdfFile);
2612             assertThat(pdfFile.exists()).isTrue();
2613             assertThat(MediaStore.scanFile(getContentResolver(), pdfFile)).isNotNull();
2614 
2615             assertThat(listAs(APP_A_HAS_RES, getDcimDir().getPath()))
2616                     .doesNotContain(NONMEDIA_FILE_NAME);
2617 
2618             createFileAsLegacyApp(videoFile);
2619             // We don't insert files to db for files created by shell.
2620             assertThat(MediaStore.scanFile(getContentResolver(), videoFile)).isNotNull();
2621             // TEST_APP_A with storage permission should see video file in Music directory.
2622             assertThat(listAs(APP_A_HAS_RES, getMusicDir().getPath())).contains(VIDEO_FILE_NAME);
2623         } finally {
2624             deleteAsLegacyApp(pdfFile);
2625             deleteAsLegacyApp(videoFile);
2626             MediaStore.scanFile(getContentResolver(), pdfFile);
2627             MediaStore.scanFile(getContentResolver(), videoFile);
2628         }
2629     }
2630 
2631     /**
2632      * Test that normal apps cannot access Android/data and Android/obb dirs of other apps
2633      */
2634     @Test
testCantAccessOtherAppsExternalDirs()2635     public void testCantAccessOtherAppsExternalDirs() throws Exception {
2636         File[] obbDirs = getContext().getObbDirs();
2637         File[] dataDirs = getContext().getExternalFilesDirs(null);
2638         for (File obbDir : obbDirs) {
2639             final File otherAppExternalObbDir = new File(obbDir.getPath().replace(
2640                     THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
2641             final File file = new File(otherAppExternalObbDir, NONMEDIA_FILE_NAME);
2642             try {
2643                 assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
2644                 assertCannotReadOrWrite(file);
2645             } finally {
2646                 deleteFileAsNoThrow(APP_B_NO_PERMS, file.getAbsolutePath());
2647             }
2648         }
2649         for (File dataDir : dataDirs) {
2650             final File otherAppExternalDataDir = new File(dataDir.getPath().replace(
2651                     THIS_PACKAGE_NAME, APP_B_NO_PERMS.getPackageName()));
2652             final File file = new File(otherAppExternalDataDir, NONMEDIA_FILE_NAME);
2653             try {
2654                 assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
2655                 assertCannotReadOrWrite(file);
2656             } finally {
2657                 deleteFileAsNoThrow(APP_B_NO_PERMS, file.getAbsolutePath());
2658             }
2659         }
2660     }
2661 
2662     /**
2663      * Test that apps can't set attributes on another app's files.
2664      */
2665     @Test
testCantSetAttrOtherAppsFile()2666     public void testCantSetAttrOtherAppsFile() throws Exception {
2667         // This path's permission is checked in MediaProvider (directory/external media dir)
2668         final File externalMediaPath = new File(getExternalMediaDir(), VIDEO_FILE_NAME);
2669 
2670         try {
2671             // Create the files
2672             if (!externalMediaPath.exists()) {
2673                 assertThat(externalMediaPath.createNewFile()).isTrue();
2674             }
2675 
2676             // APP A should not be able to setattr to other app's files.
2677             assertWithMessage(
2678                     "setattr on directory/external media path [%s]", externalMediaPath.getPath())
2679                     .that(setAttrAs(APP_A_HAS_RES, externalMediaPath.getPath()))
2680                     .isFalse();
2681         } finally {
2682             externalMediaPath.delete();
2683         }
2684     }
2685 
2686     /**
2687      * b/171768780: Test that scan doesn't skip scanning renamed hidden file.
2688      */
2689     @Test
testScanUpdatesMetadataForRenamedHiddenFile()2690     public void testScanUpdatesMetadataForRenamedHiddenFile() throws Exception {
2691         final File hiddenFile = new File(getPicturesDir(), ".hidden_" + IMAGE_FILE_NAME);
2692         final File jpgFile = new File(getPicturesDir(), IMAGE_FILE_NAME);
2693         try {
2694             // Copy the image content to hidden file
2695             try (InputStream in =
2696                          getContext().getResources().openRawResource(R.raw.img_with_metadata);
2697                  FileOutputStream out = new FileOutputStream(hiddenFile)) {
2698                 FileUtils.copy(in, out);
2699                 out.getFD().sync();
2700             }
2701             Uri scanUri = MediaStore.scanFile(getContentResolver(), hiddenFile);
2702             assertNotNull(scanUri);
2703 
2704             // Rename hidden file to non-hidden
2705             assertCanRenameFile(hiddenFile, jpgFile);
2706 
2707             try (Cursor c = queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
2708                 assertTrue(c.moveToFirst());
2709                 // The file is not scanned yet, hence the metadata is not updated yet.
2710                 assertThat(c.getString(0)).isNull();
2711             }
2712 
2713             // Scan the file to update the metadata for renamed hidden file.
2714             scanUri = MediaStore.scanFile(getContentResolver(), jpgFile);
2715             assertNotNull(scanUri);
2716 
2717             // Scan should be able to update metadata even if File.lastModifiedTime hasn't changed.
2718             try (Cursor c = queryFile(jpgFile, MediaStore.MediaColumns.DATE_TAKEN)) {
2719                 assertTrue(c.moveToFirst());
2720                 assertThat(c.getString(0)).isNotNull();
2721             }
2722         } finally {
2723             hiddenFile.delete();
2724             jpgFile.delete();
2725         }
2726     }
2727 
2728     @Test
testInsertFromExternalDirsViaRelativePath()2729     public void testInsertFromExternalDirsViaRelativePath() throws Exception {
2730         verifyInsertFromExternalMediaDirViaRelativePath_allowed();
2731         verifyInsertFromExternalPrivateDirViaRelativePath_denied();
2732     }
2733 
2734     @Test
testUpdateToExternalDirsViaRelativePath()2735     public void testUpdateToExternalDirsViaRelativePath() throws Exception {
2736         verifyUpdateToExternalMediaDirViaRelativePath_allowed();
2737         verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
2738     }
2739 
2740     @Test
testInsertFromExternalDirsViaRelativePathAsSystemGallery()2741     public void testInsertFromExternalDirsViaRelativePathAsSystemGallery() throws Exception {
2742         int uid = Process.myUid();
2743         try {
2744             setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
2745             verifyInsertFromExternalMediaDirViaRelativePath_allowed();
2746             verifyInsertFromExternalPrivateDirViaRelativePath_denied();
2747         } finally {
2748             setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
2749         }
2750     }
2751 
2752     @Test
testUpdateToExternalDirsViaRelativePathAsSystemGallery()2753     public void testUpdateToExternalDirsViaRelativePathAsSystemGallery() throws Exception {
2754         int uid = Process.myUid();
2755         try {
2756             setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
2757             verifyUpdateToExternalMediaDirViaRelativePath_allowed();
2758             verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
2759         } finally {
2760             setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
2761         }
2762     }
2763 
2764     @Test
testDeferredScanHidesPartialDatabaseRows()2765     public void testDeferredScanHidesPartialDatabaseRows() throws Exception {
2766         ContentValues values = new ContentValues();
2767         values.put(MediaStore.MediaColumns.IS_PENDING, 1);
2768         // Insert a pending row
2769         final Uri targetUri = getContentResolver().insert(getImageContentUri(), values, null);
2770         try (InputStream in =
2771                      getContext().getResources().openRawResource(R.raw.img_with_metadata)) {
2772             try (ParcelFileDescriptor pfd =
2773                          getContentResolver().openFileDescriptor(targetUri, "w")) {
2774                 // Write image content to the file
2775                 FileUtils.copy(in, new ParcelFileDescriptor.AutoCloseOutputStream(pfd));
2776             }
2777         }
2778 
2779         // Verify that metadata is not updated yet.
2780         try (Cursor c = getContentResolver().query(targetUri, new String[] {
2781                 MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null)) {
2782             assertThat(c.moveToFirst()).isTrue();
2783             assertThat(c.getString(0)).isNull();
2784         }
2785         // Get file path to use in the next query().
2786         final String imageFilePath = getFilePathFromUri(targetUri);
2787 
2788         values.put(MediaStore.MediaColumns.IS_PENDING, 0);
2789         Bundle extras = new Bundle();
2790         extras.putBoolean(MediaStore.QUERY_ARG_DEFER_SCAN, true);
2791         // Publish the file, but, defer the scan on update().
2792         assertThat(getContentResolver().update(targetUri, values, extras)).isEqualTo(1);
2793 
2794         // The update() above can return before scanning is complete. Verify that either we don't
2795         // see the file in published files or if the file appears in the collection, it means that
2796         // deferred scan is now complete, hence verify metadata is intact.
2797         try (Cursor c = getContentResolver().query(getImageContentUri(),
2798                 new String[] {MediaStore.Images.ImageColumns.DATE_TAKEN},
2799                 MediaStore.Files.FileColumns.DATA + "=?", new String[] {imageFilePath}, null)) {
2800             if (c.getCount() == 1) {
2801                 // If the file appears in media collection as published file, verify that metadata
2802                 // is correct.
2803                 assertThat(c.moveToFirst()).isTrue();
2804                 assertThat(c.getString(0)).isNotNull();
2805                 Log.i(TAG, "Verified that deferred scan on " + imageFilePath + " is complete"
2806                         + " and hence metadata is updated");
2807 
2808             } else {
2809                 assertThat(c.getCount()).isEqualTo(0);
2810                 Log.i(TAG, "Verified that " + imageFilePath + " was excluded in default query");
2811             }
2812         }
2813     }
2814 
2815     @Test
2816     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testTransformsDirFileOperations()2817     public void testTransformsDirFileOperations() throws Exception {
2818         final String path = Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_DIR;
2819         final File file = new File(path);
2820         assertThat(file.exists()).isTrue();
2821         testTransformsDirCommon(file);
2822     }
2823 
2824     @Test
2825     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testTransformsSyntheticDirFileOperations()2826     public void testTransformsSyntheticDirFileOperations() throws Exception {
2827         final String path =
2828                 Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_SYNTHETIC_DIR;
2829         final File file = new File(path);
2830         assertThat(file.exists()).isTrue();
2831         testTransformsDirCommon(file);
2832     }
2833 
2834     @Test
2835     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testTransformsTranscodeDirFileOperations()2836     public void testTransformsTranscodeDirFileOperations() throws Exception {
2837         final String path =
2838                 Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_TRANSCODE_DIR;
2839         final File file = new File(path);
2840         assertThat(file.exists()).isFalse();
2841         testTransformsDirCommon(file);
2842     }
2843 
2844 
2845     /**
2846      * Test mount modes for a platform signed app with ACCESS_MTP permission.
2847      */
2848     @Test
2849     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testMTPAppWithPlatformSignatureMountMode()2850     public void testMTPAppWithPlatformSignatureMountMode() throws Exception {
2851         final String shellPackageName = "com.android.shell";
2852         final int uid = getContext().getPackageManager().getPackageUid(shellPackageName, 0);
2853         assertMountMode(shellPackageName, uid, StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE);
2854     }
2855 
2856     /**
2857      * Test mount modes for ExternalStorageProvider and DownloadsProvider.
2858      */
2859     @Test
2860     @SdkSuppress(minSdkVersion = 31, codeName = "S")
testExternalStorageProviderAndDownloadsProvider()2861     public void testExternalStorageProviderAndDownloadsProvider() throws Exception {
2862         assertWritableMountModeForProvider(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY);
2863         assertWritableMountModeForProvider(DocumentsContract.DOWNLOADS_PROVIDER_AUTHORITY);
2864     }
2865 
assertWritableMountModeForProvider(String auth)2866     private void assertWritableMountModeForProvider(String auth) {
2867         final ProviderInfo provider = getContext().getPackageManager()
2868                 .resolveContentProvider(auth, 0);
2869         int uid = provider.applicationInfo.uid;
2870         final String packageName = provider.applicationInfo.packageName;
2871 
2872         assertMountMode(packageName, uid, StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE);
2873     }
2874 
canRenameFile(File file)2875     private boolean canRenameFile(File file) {
2876         return file.renameTo(new File(file.getAbsolutePath() + "test"));
2877     }
2878 
testTransformsDirCommon(File file)2879     private void testTransformsDirCommon(File file) throws Exception {
2880         assertThat(file.delete()).isFalse();
2881         assertThat(canRenameFile(file)).isFalse();
2882 
2883         final File newFile = new File(file.getAbsolutePath(), "test");
2884         assertThat(newFile.mkdir()).isFalse();
2885         assertThrows(IOException.class, () -> newFile.createNewFile());
2886     }
2887 
assertCanWriteAndRead(File file, byte[] data)2888     private void assertCanWriteAndRead(File file, byte[] data) throws Exception {
2889         // Assert we can write to images/videos
2890         try (FileOutputStream fos = new FileOutputStream(file)) {
2891             fos.write(data);
2892         }
2893         assertFileContent(file, data);
2894     }
2895 
2896     /**
2897      * Checks restrictions for opening pending and trashed files by different apps. Assumes that
2898      * given {@code testApp} is already installed and has READ_EXTERNAL_STORAGE permission. This
2899      * method doesn't uninstall given {@code testApp} at the end.
2900      */
assertOpenPendingOrTrashed(Uri uri, boolean isImageOrVideo)2901     private void assertOpenPendingOrTrashed(Uri uri, boolean isImageOrVideo)
2902             throws Exception {
2903         final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
2904 
2905         // App can open its pending or trashed file for read or write
2906         assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ false));
2907         assertTrue(canOpen(pendingOrTrashedFile, /*forWrite*/ true));
2908 
2909         // App with READ_EXTERNAL_STORAGE can't open other app's pending or trashed file for read or
2910         // write
2911         assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
2912         assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
2913 
2914         assertTrue(canOpenFileAs(APP_FM, pendingOrTrashedFile, /*forWrite*/ false));
2915         assertTrue(canOpenFileAs(APP_FM, pendingOrTrashedFile, /*forWrite*/ true));
2916 
2917         final int resAppUid =
2918                 getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(), 0);
2919         try {
2920             allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2921             if (isImageOrVideo) {
2922                 // System Gallery can open any pending or trashed image/video file for read or write
2923                 assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
2924                 assertTrue(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
2925                 assertTrue(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
2926             } else {
2927                 // System Gallery can't open other app's pending or trashed non-media file for read
2928                 // or write
2929                 assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
2930                 assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ false));
2931                 assertFalse(canOpenFileAs(APP_A_HAS_RES, pendingOrTrashedFile, /*forWrite*/ true));
2932             }
2933         } finally {
2934             denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2935         }
2936     }
2937 
2938     /**
2939      * Checks restrictions for listing pending and trashed files by different apps.
2940      */
assertListPendingOrTrashed(Uri uri, File file, boolean isImageOrVideo)2941     private void assertListPendingOrTrashed(Uri uri, File file, boolean isImageOrVideo)
2942             throws Exception {
2943         final String parentDirPath = file.getParent();
2944         assertTrue(new File(parentDirPath).isDirectory());
2945 
2946         final List<String> listedFileNames = Arrays.asList(new File(parentDirPath).list());
2947         assertThat(listedFileNames).doesNotContain(file);
2948 
2949         final File pendingOrTrashedFile = new File(getFilePathFromUri(uri));
2950 
2951         assertThat(listedFileNames).contains(pendingOrTrashedFile.getName());
2952 
2953         // App with READ_EXTERNAL_STORAGE can't see other app's pending or trashed file.
2954         assertThat(listAs(APP_A_HAS_RES, parentDirPath)).doesNotContain(
2955                 pendingOrTrashedFile.getName());
2956 
2957         final int resAppUid =
2958                 getContext().getPackageManager().getPackageUid(APP_A_HAS_RES.getPackageName(), 0);
2959         // File Manager can see any pending or trashed file.
2960         assertThat(listAs(APP_FM, parentDirPath)).contains(pendingOrTrashedFile.getName());
2961 
2962 
2963         try {
2964             allowAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2965             if (isImageOrVideo) {
2966                 // System Gallery can see any pending or trashed image/video file.
2967                 assertTrue(isMediaTypeImageOrVideo(pendingOrTrashedFile));
2968                 assertThat(listAs(APP_A_HAS_RES, parentDirPath)).contains(
2969                         pendingOrTrashedFile.getName());
2970             } else {
2971                 // System Gallery can't see other app's pending or trashed non media file.
2972                 assertFalse(isMediaTypeImageOrVideo(pendingOrTrashedFile));
2973                 assertThat(listAs(APP_A_HAS_RES, parentDirPath))
2974                         .doesNotContain(pendingOrTrashedFile.getName());
2975             }
2976         } finally {
2977             denyAppOpsToUid(resAppUid, SYSTEM_GALERY_APPOPS);
2978         }
2979     }
2980 
createPendingFile(File pendingFile)2981     private Uri createPendingFile(File pendingFile) throws Exception {
2982         assertTrue(pendingFile.createNewFile());
2983 
2984         final ContentResolver cr = getContentResolver();
2985         final Uri trashedFileUri = MediaStore.scanFile(cr, pendingFile);
2986         assertNotNull(trashedFileUri);
2987 
2988         final ContentValues values = new ContentValues();
2989         values.put(MediaStore.MediaColumns.IS_PENDING, 1);
2990         assertEquals(1, cr.update(trashedFileUri, values, Bundle.EMPTY));
2991 
2992         return trashedFileUri;
2993     }
2994 
createTrashedFile(File trashedFile)2995     private Uri createTrashedFile(File trashedFile) throws Exception {
2996         assertTrue(trashedFile.createNewFile());
2997 
2998         final ContentResolver cr = getContentResolver();
2999         final Uri trashedFileUri = MediaStore.scanFile(cr, trashedFile);
3000         assertNotNull(trashedFileUri);
3001 
3002         trashFile(trashedFileUri);
3003         return trashedFileUri;
3004     }
3005 
trashFile(Uri uri)3006     private void trashFile(Uri uri) throws Exception {
3007         final ContentValues values = new ContentValues();
3008         values.put(MediaStore.MediaColumns.IS_TRASHED, 1);
3009         assertEquals(1, getContentResolver().update(uri, values, Bundle.EMPTY));
3010     }
3011 
3012     /**
3013      * Gets file path corresponding to the db row pointed by {@code uri}. If {@code uri} points to
3014      * multiple db rows, file path is extracted from the first db row of the database query result.
3015      */
getFilePathFromUri(Uri uri)3016     private String getFilePathFromUri(Uri uri) {
3017         final String[] projection = new String[] {MediaStore.MediaColumns.DATA};
3018         try (Cursor c = getContentResolver().query(uri, projection, null, null)) {
3019             assertTrue(c.moveToFirst());
3020             return c.getString(0);
3021         }
3022     }
3023 
isMediaTypeImageOrVideo(File file)3024     private boolean isMediaTypeImageOrVideo(File file) {
3025         return queryImageFile(file).getCount() == 1 || queryVideoFile(file).getCount() == 1;
3026     }
3027 
assertIsMediaTypeImage(File file)3028     private static void assertIsMediaTypeImage(File file) {
3029         final Cursor c = queryImageFile(file);
3030         assertEquals(1, c.getCount());
3031     }
3032 
assertNotMediaTypeImage(File file)3033     private static void assertNotMediaTypeImage(File file) {
3034         final Cursor c = queryImageFile(file);
3035         assertEquals(0, c.getCount());
3036     }
3037 
assertCantQueryFile(File file)3038     private static void assertCantQueryFile(File file) {
3039         assertThat(getFileUri(file)).isNull();
3040         // Confirm that file exists in the database.
3041         assertNotNull(MediaStore.scanFile(getContentResolver(), file));
3042     }
3043 
assertCreateFilesAs(TestApp testApp, File... files)3044     private static void assertCreateFilesAs(TestApp testApp, File... files) throws Exception {
3045         for (File file : files) {
3046             assertFalse("File already exists: " + file, file.exists());
3047             assertTrue("Failed to create file " + file + " on behalf of "
3048                     + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
3049         }
3050     }
3051 
3052     /**
3053      * Makes {@code testApp} create {@code files}. Publishes {@code files} by scanning the file.
3054      * Pending files from FUSE are not visible to other apps via MediaStore APIs. We have to publish
3055      * the file or make the file non-pending to make the file visible to other apps.
3056      * <p>
3057      * Note that this method can only be used for scannable files.
3058      */
assertCreatePublishedFilesAs(TestApp testApp, File... files)3059     private static void assertCreatePublishedFilesAs(TestApp testApp, File... files)
3060             throws Exception {
3061         for (File file : files) {
3062             assertTrue("Failed to create published file " + file + " on behalf of "
3063                     + testApp.getPackageName(), createFileAs(testApp, file.getPath()));
3064             assertNotNull("Failed to scan " + file,
3065                     MediaStore.scanFile(getContentResolver(), file));
3066         }
3067     }
3068 
3069 
deleteFilesAs(TestApp testApp, File... files)3070     private static void deleteFilesAs(TestApp testApp, File... files) throws Exception {
3071         for (File file : files) {
3072             deleteFileAs(testApp, file.getPath());
3073         }
3074     }
assertCanDeletePathsAs(TestApp testApp, String... filePaths)3075     private static void assertCanDeletePathsAs(TestApp testApp, String... filePaths)
3076             throws Exception {
3077         for (String path: filePaths) {
3078             assertTrue("Failed to delete file " + path + " on behalf of "
3079                     + testApp.getPackageName(), deleteFileAs(testApp, path));
3080         }
3081     }
3082 
assertCantDeletePathsAs(TestApp testApp, String... filePaths)3083     private static void assertCantDeletePathsAs(TestApp testApp, String... filePaths)
3084             throws Exception {
3085         for (String path: filePaths) {
3086             assertFalse("Deleting " + path + " on behalf of " + testApp.getPackageName()
3087                     + " was expected to fail", deleteFileAs(testApp, path));
3088         }
3089     }
3090 
deleteFiles(File... files)3091     private void deleteFiles(File... files) {
3092         for (File file: files) {
3093             if (file == null) continue;
3094             file.delete();
3095         }
3096     }
3097 
deletePaths(String... paths)3098     private void deletePaths(String... paths) {
3099         for (String path: paths) {
3100             if (path == null) continue;
3101             new File(path).delete();
3102         }
3103     }
3104 
assertCanDeletePaths(String... filePaths)3105     private static void assertCanDeletePaths(String... filePaths) {
3106         for (String filePath : filePaths) {
3107             assertTrue("Failed to delete " + filePath,
3108                     new File(filePath).delete());
3109         }
3110     }
3111 
3112     /**
3113      * For possible values of {@code mode}, look at {@link android.content.ContentProvider#openFile}
3114      */
assertCanQueryAndOpenFile(File file, String mode)3115     private static void assertCanQueryAndOpenFile(File file, String mode) throws IOException {
3116         // This call performs the query
3117         final Uri fileUri = getFileUri(file);
3118         // The query succeeds iff it didn't return null
3119         assertThat(fileUri).isNotNull();
3120         // Now we assert that we can open the file through ContentResolver
3121         try (ParcelFileDescriptor pfd =
3122                      getContentResolver().openFileDescriptor(fileUri, mode)) {
3123             assertThat(pfd).isNotNull();
3124         }
3125     }
3126 
3127     /**
3128      * Assert that the last read in: read - write - read using {@code readFd} and {@code writeFd}
3129      * see the last write. {@code readFd} and {@code writeFd} are fds pointing to the same
3130      * underlying file on disk but may be derived from different mount points and in that case
3131      * have separate VFS caches.
3132      */
assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)3133     private void assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)
3134             throws Exception {
3135         FileDescriptor readFd = readPfd.getFileDescriptor();
3136         FileDescriptor writeFd = writePfd.getFileDescriptor();
3137 
3138         byte[] readBuffer = new byte[10];
3139         byte[] writeBuffer = new byte[10];
3140         Arrays.fill(writeBuffer, (byte) 1);
3141 
3142         // Write so readFd has content to read from next
3143         Os.pwrite(readFd, readBuffer, 0, 10, 0);
3144         // Read so readBuffer is in readFd's mount VFS cache
3145         Os.pread(readFd, readBuffer, 0, 10, 0);
3146 
3147         // Assert that readBuffer is zeroes
3148         assertThat(readBuffer).isEqualTo(new byte[10]);
3149 
3150         // Write so writeFd and readFd should now see writeBuffer
3151         Os.pwrite(writeFd, writeBuffer, 0, 10, 0);
3152 
3153         // Read so the last write can be verified on readFd
3154         Os.pread(readFd, readBuffer, 0, 10, 0);
3155 
3156         // Assert that the last write is indeed visible via readFd
3157         assertThat(readBuffer).isEqualTo(writeBuffer);
3158         assertThat(readPfd.getStatSize()).isEqualTo(writePfd.getStatSize());
3159     }
3160 
assertStartsWith(String actual, String prefix)3161     private void assertStartsWith(String actual, String prefix) throws Exception {
3162         String message = "String \"" + actual + "\" should start with \"" + prefix + "\"";
3163 
3164         assertWithMessage(message).that(actual).startsWith(prefix);
3165     }
3166 
assertLowerFsFd(ParcelFileDescriptor pfd)3167     private void assertLowerFsFd(ParcelFileDescriptor pfd) throws Exception {
3168         String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
3169         String prefix = "/storage";
3170 
3171         assertStartsWith(path, prefix);
3172     }
3173 
assertUpperFsFd(ParcelFileDescriptor pfd)3174     private void assertUpperFsFd(ParcelFileDescriptor pfd) throws Exception {
3175         String path = Os.readlink("/proc/self/fd/" + pfd.getFd());
3176         String prefix = "/mnt/user";
3177 
3178         assertStartsWith(path, prefix);
3179     }
3180 
assertLowerFsFdWithPassthrough(ParcelFileDescriptor pfd)3181     private void assertLowerFsFdWithPassthrough(ParcelFileDescriptor pfd) throws Exception {
3182         if (getBoolean("persist.sys.fuse.passthrough.enable", false)) {
3183             assertUpperFsFd(pfd);
3184         } else {
3185             assertLowerFsFd(pfd);
3186         }
3187     }
3188 
assertCanCreateFile(File file)3189     private static void assertCanCreateFile(File file) throws IOException {
3190         // If the file somehow managed to survive a previous run, then the test app was uninstalled
3191         // and MediaProvider will remove our its ownership of the file, so it's not guaranteed that
3192         // we can create nor delete it.
3193         if (!file.exists()) {
3194             assertThat(file.createNewFile()).isTrue();
3195             assertThat(file.delete()).isTrue();
3196         } else {
3197             Log.w(TAG,
3198                     "Couldn't assertCanCreateFile(" + file + ") because file existed prior to "
3199                             + "running the test!");
3200         }
3201     }
3202 
assertCannotReadOrWrite(File file)3203     private static void assertCannotReadOrWrite(File file)
3204             throws Exception {
3205         // App data directories have different 'x' bits on upgrading vs new devices. Let's not
3206         // check 'exists', by passing checkExists=false. But assert this app cannot read or write
3207         // the other app's file.
3208         assertAccess(file, false /* value is moot */, false /* canRead */,
3209                 false /* canWrite */, false /* checkExists */);
3210     }
3211 
assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)3212     private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)
3213             throws Exception {
3214         assertAccess(file, exists, canRead, canWrite, true /* checkExists */);
3215     }
3216 
assertAccess(File file, boolean exists, boolean canRead, boolean canWrite, boolean checkExists)3217     private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite,
3218             boolean checkExists) throws Exception {
3219         if (checkExists) {
3220             assertThat(file.exists()).isEqualTo(exists);
3221         }
3222         assertThat(file.canRead()).isEqualTo(canRead);
3223         assertThat(file.canWrite()).isEqualTo(canWrite);
3224         if (file.isDirectory()) {
3225             if (checkExists) {
3226                 assertThat(file.canExecute()).isEqualTo(exists);
3227             }
3228         } else {
3229             assertThat(file.canExecute()).isFalse(); // Filesytem is mounted with MS_NOEXEC
3230         }
3231 
3232         // Test some combinations of mask.
3233         assertAccess(file, R_OK, canRead);
3234         assertAccess(file, W_OK, canWrite);
3235         assertAccess(file, R_OK | W_OK, canRead && canWrite);
3236         assertAccess(file, W_OK | F_OK, canWrite);
3237 
3238         if (checkExists) {
3239             assertAccess(file, F_OK, exists);
3240         }
3241     }
3242 
assertAccess(File file, int mask, boolean expected)3243     private static void assertAccess(File file, int mask, boolean expected) throws Exception {
3244         if (expected) {
3245             assertThat(Os.access(file.getAbsolutePath(), mask)).isTrue();
3246         } else {
3247             assertThrows(ErrnoException.class, () -> {
3248                 Os.access(file.getAbsolutePath(), mask);
3249             });
3250         }
3251     }
3252 
3253     /**
3254      * Creates a file at any location on storage (except external app data directory).
3255      * The owner of the file is not the caller app.
3256      */
createFileAsLegacyApp(File file)3257     private void createFileAsLegacyApp(File file) throws Exception {
3258         // Use a legacy app to create this file, since it could be outside shared storage.
3259         Log.d(TAG, "Creating file " + file);
3260         assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath())).isTrue();
3261     }
3262 
3263     /**
3264      * Creates a file at any location on storage (except external app data directory).
3265      * The owner of the file is not the caller app.
3266      */
createDirectoryAsLegacyApp(File file)3267     private void createDirectoryAsLegacyApp(File file) throws Exception {
3268         // Use a legacy app to create this file, since it could be outside shared storage.
3269         Log.d(TAG, "Creating directory " + file);
3270         // Create a tmp file in the target directory, this would also create the required
3271         // directory, then delete the tmp file. It would leave only new directory.
3272         assertThat(createFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
3273         assertThat(deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath() + "/tmp.txt")).isTrue();
3274     }
3275 
3276     /**
3277      * Deletes a file or directory at any location on storage (except external app data directory).
3278      */
deleteAsLegacyApp(File file)3279     private void deleteAsLegacyApp(File file) throws Exception {
3280         // Use a legacy app to delete this file, since it could be outside shared storage.
3281         Log.d(TAG, "Deleting file " + file);
3282         deleteFileAs(APP_D_LEGACY_HAS_RW, file.getAbsolutePath());
3283     }
3284 }
3285