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