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