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