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