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