| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.scopedstorage.cts.legacy; |
| |
| import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA1; |
| import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2; |
| import static android.scopedstorage.cts.lib.TestUtils.STR_DATA1; |
| import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2; |
| import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameDirectory; |
| import static android.scopedstorage.cts.lib.TestUtils.assertCanRenameFile; |
| import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile; |
| import static android.scopedstorage.cts.lib.TestUtils.assertDirectoryContains; |
| import static android.scopedstorage.cts.lib.TestUtils.assertFileContent; |
| import static android.scopedstorage.cts.lib.TestUtils.createFileAs; |
| import static android.scopedstorage.cts.lib.TestUtils.deleteFileAsNoThrow; |
| import static android.scopedstorage.cts.lib.TestUtils.executeShellCommand; |
| import static android.scopedstorage.cts.lib.TestUtils.getContentResolver; |
| import static android.scopedstorage.cts.lib.TestUtils.getFileOwnerPackageFromDatabase; |
| import static android.scopedstorage.cts.lib.TestUtils.getFileRowIdFromDatabase; |
| import static android.scopedstorage.cts.lib.TestUtils.installApp; |
| import static android.scopedstorage.cts.lib.TestUtils.listAs; |
| import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState; |
| import static android.scopedstorage.cts.lib.TestUtils.pollForPermission; |
| import static android.scopedstorage.cts.lib.TestUtils.setupDefaultDirectories; |
| import static android.scopedstorage.cts.lib.TestUtils.uninstallApp; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.Manifest; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.provider.MediaStore; |
| import android.scopedstorage.cts.lib.TestUtils; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.cts.install.lib.TestApp; |
| |
| import com.google.common.io.Files; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| |
| /** |
| * Test app targeting Q and requesting legacy storage - tests legacy file path access. |
| * Designed to be run by LegacyAccessHostTest. |
| * |
| * <p> Test cases that assume we have WRITE_EXTERNAL_STORAGE only are appended with hasW, |
| * those that assume we have READ_EXTERNAL_STORAGE only are appended with hasR, those who assume we |
| * have both are appended with hasRW. |
| */ |
| @RunWith(AndroidJUnit4.class) |
| public class LegacyStorageTest { |
| private static final String TAG = "LegacyFileAccessTest"; |
| static final String THIS_PACKAGE_NAME = InstrumentationRegistry.getContext().getPackageName(); |
| |
| static final String IMAGE_FILE_NAME = "LegacyStorageTest_file.jpg"; |
| static final String VIDEO_FILE_NAME = "LegacyStorageTest_file.mp4"; |
| static final String NONMEDIA_FILE_NAME = "LegacyStorageTest_file.pdf"; |
| |
| private static final TestApp TEST_APP_A = new TestApp("TestAppA", |
| "android.scopedstorage.cts.testapp.A", 1, false, "CtsScopedStorageTestAppA.apk"); |
| |
| /** |
| * This method needs to be called once before running the whole test. |
| */ |
| @Test |
| public void setupExternalStorage() { |
| setupDefaultDirectories(); |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| pollForExternalStorageState(); |
| } |
| |
| /** |
| * Tests that legacy apps bypass the type-path conformity restrictions imposed by |
| * MediaProvider. <p> Assumes we have WRITE_EXTERNAL_STORAGE. |
| */ |
| @Test |
| public void testCreateFilesInRandomPlaces_hasW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| // Can create file under root dir |
| assertCanCreateFile(new File(TestUtils.getExternalStorageDir(), "LegacyFileAccessTest.txt")); |
| |
| // Can create music file under DCIM |
| assertCanCreateFile(new File(TestUtils.getDcimDir(), "LegacyFileAccessTest.mp3")); |
| |
| // Can create random file under external files dir |
| assertCanCreateFile(new File(TestUtils.getExternalFilesDir(), |
| "LegacyFileAccessTest")); |
| |
| // However, even legacy apps can't create files under other app's directories |
| final File otherAppDir = new File(TestUtils.getAndroidDataDir(), "com.android.shell"); |
| final File file = new File(otherAppDir, "LegacyFileAccessTest.txt"); |
| |
| // otherAppDir was already created by the host test |
| try { |
| file.createNewFile(); |
| fail("File creation expected to fail: " + file); |
| } catch (IOException expected) { |
| } |
| } |
| |
| /** |
| * Tests that legacy apps bypass dir creation/deletion restrictions imposed by MediaProvider. |
| * <p> Assumes we have WRITE_EXTERNAL_STORAGE. |
| */ |
| @Test |
| public void testMkdirInRandomPlaces_hasW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| // Can create a top-level direcotry |
| final File topLevelDir = new File(TestUtils.getExternalStorageDir(), "LegacyFileAccessTest"); |
| assertCanCreateDir(topLevelDir); |
| |
| final File otherAppDir = new File(TestUtils.getAndroidDataDir(), "com.android.shell"); |
| |
| // However, even legacy apps can't create dirs under other app's directories |
| final File subDir = new File(otherAppDir, "LegacyFileAccessTest"); |
| // otherAppDir was already created by the host test |
| assertThat(subDir.mkdir()).isFalse(); |
| |
| // Try to list a directory and fail because it requires READ permission |
| assertThat(TestUtils.getMusicDir().list()).isNull(); |
| } |
| |
| /** |
| * Tests that an app can't access external storage without permissions. |
| */ |
| @Test |
| public void testCantAccessExternalStorage() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false); |
| // Can't create file under root dir |
| final File newTxtFile = new File(TestUtils.getExternalStorageDir(), |
| "LegacyFileAccessTest.txt"); |
| try { |
| newTxtFile.createNewFile(); |
| fail("File creation expected to fail: " + newTxtFile); |
| } catch (IOException expected) { |
| } |
| |
| // Can't create music file under /MUSIC |
| final File newMusicFile = new File(TestUtils.getMusicDir(), "LegacyFileAccessTest.mp3"); |
| try { |
| newMusicFile.createNewFile(); |
| fail("File creation expected to fail: " + newMusicFile); |
| } catch (IOException expected) { |
| } |
| |
| // Can't create a top-level direcotry |
| final File topLevelDir = new File(TestUtils.getExternalStorageDir(), "LegacyFileAccessTest"); |
| assertThat(topLevelDir.mkdir()).isFalse(); |
| |
| // Can't read existing file |
| final File existingFile = new File(TestUtils.getExternalStorageDir(), |
| "LegacyAccessHostTest_shell"); |
| try { |
| Os.open(existingFile.getPath(), OsConstants.O_RDONLY, /*mode*/ 0); |
| fail("Opening file for read expected to fail: " + existingFile); |
| } catch (ErrnoException expected) { |
| } |
| |
| // Can't delete file |
| assertThat(existingFile.delete()).isFalse(); |
| |
| // try to list a directory and fail |
| assertThat(TestUtils.getMusicDir().list()).isNull(); |
| assertThat(TestUtils.getExternalStorageDir().list()).isNull(); |
| |
| // However, even without permissions, we can access our own external dir |
| final File fileInDataDir = |
| new File(TestUtils.getExternalFilesDir(), |
| "LegacyFileAccessTest"); |
| try { |
| assertThat(fileInDataDir.createNewFile()).isTrue(); |
| assertThat(Arrays.asList(fileInDataDir.getParentFile().list())) |
| .contains("LegacyFileAccessTest"); |
| } finally { |
| fileInDataDir.delete(); |
| } |
| |
| // we can access our own external media directory without permissions. |
| final File fileInMediaDir = |
| new File(TestUtils.getExternalMediaDir(), |
| "LegacyFileAccessTest"); |
| try { |
| assertThat(fileInMediaDir.createNewFile()).isTrue(); |
| assertThat(Arrays.asList(fileInMediaDir.getParentFile().list())) |
| .contains("LegacyFileAccessTest"); |
| } finally { |
| fileInMediaDir.delete(); |
| } |
| } |
| |
| // test read storage permission |
| @Test |
| public void testReadOnlyExternalStorage_hasR() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false); |
| // can list directory content |
| assertThat(TestUtils.getMusicDir().list()).isNotNull(); |
| |
| // try to write a file and fail |
| final File existingFile = new File(TestUtils.getExternalStorageDir(), |
| "LegacyAccessHostTest_shell"); |
| |
| // can open file for read |
| FileDescriptor fd = null; |
| try { |
| fd = Os.open(existingFile.getPath(), OsConstants.O_RDONLY, /*mode*/ 0); |
| } finally { |
| if (fd != null) { |
| Os.close(fd); |
| } |
| } |
| |
| try { |
| fd = Os.open(existingFile.getPath(), OsConstants.O_WRONLY, /*mode*/ 0); |
| Os.close(fd); |
| fail("Opening file for write expected to fail: " + existingFile); |
| } catch (ErrnoException expected) { |
| } |
| |
| // try to create file and fail, because it requires WRITE |
| final File newFile = new File(TestUtils.getMusicDir(), "LegacyFileAccessTest.mp3"); |
| try { |
| newFile.createNewFile(); |
| fail("Creating file expected to fail: " + newFile); |
| } catch (IOException expected) { |
| } |
| |
| // try to mkdir and fail, because it requires WRITE |
| final File newDir = new File(TestUtils.getExternalStorageDir(), "LegacyFileAccessTest"); |
| try { |
| assertThat(newDir.mkdir()).isFalse(); |
| } finally { |
| newDir.delete(); |
| } |
| } |
| |
| /** |
| * Test that legacy app with storage permission can list all files |
| */ |
| @Test |
| public void testListFiles_hasR() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false); |
| |
| // can list a non-media file created by other package. |
| assertThat(Arrays.asList(TestUtils.getExternalStorageDir().list())) |
| .contains("LegacyAccessHostTest_shell"); |
| } |
| |
| /** |
| * Test that rename for legacy app with WRITE_EXTERNAL_STORAGE permission bypasses rename |
| * restrictions imposed by MediaProvider |
| */ |
| @Test |
| public void testCanRename_hasRW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File musicFile1 = new File(TestUtils.getDcimDir(), "LegacyFileAccessTest.mp3"); |
| final File musicFile2 = new File(TestUtils.getExternalStorageDir(), |
| "LegacyFileAccessTest.mp3"); |
| final File musicFile3 = new File(TestUtils.getMoviesDir(), "LegacyFileAccessTest.mp3"); |
| final File nonMediaDir1 = new File(TestUtils.getDcimDir(), "LegacyFileAccessTest"); |
| final File nonMediaDir2 = new File(TestUtils.getExternalStorageDir(), |
| "LegacyFileAccessTest"); |
| final File pdfFile1 = new File(nonMediaDir1, "LegacyFileAccessTest.pdf"); |
| final File pdfFile2 = new File(nonMediaDir2, "LegacyFileAccessTest.pdf"); |
| try { |
| // can rename a file to root directory. |
| assertThat(musicFile1.createNewFile()).isTrue(); |
| assertCanRenameFile(musicFile1, musicFile2); |
| |
| // can rename a music file to Movies directory. |
| assertCanRenameFile(musicFile2, musicFile3); |
| |
| assertThat(nonMediaDir1.mkdir()).isTrue(); |
| assertThat(pdfFile1.createNewFile()).isTrue(); |
| // can rename directory to root directory. |
| assertCanRenameDirectory( |
| nonMediaDir1, nonMediaDir2, new File[] {pdfFile1}, new File[] {pdfFile2}); |
| } finally { |
| musicFile1.delete(); |
| musicFile2.delete(); |
| musicFile3.delete(); |
| |
| pdfFile1.delete(); |
| pdfFile2.delete(); |
| nonMediaDir1.delete(); |
| nonMediaDir2.delete(); |
| } |
| } |
| |
| /** |
| * Test that legacy app with only READ_EXTERNAL_STORAGE can only rename files in app external |
| * directories. |
| */ |
| @Test |
| public void testCantRename_hasR() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false); |
| |
| final File shellFile1 = new File(TestUtils.getExternalStorageDir(), |
| "LegacyAccessHostTest_shell"); |
| final File shellFile2 = new File(TestUtils.getDownloadDir(), "LegacyFileAccessTest_shell"); |
| final File mediaFile1 = |
| new File(TestUtils.getExternalMediaDir(), |
| "LegacyFileAccessTest1"); |
| final File mediaFile2 = |
| new File(TestUtils.getExternalMediaDir(), |
| "LegacyFileAccessTest2"); |
| try { |
| // app can't rename shell file. |
| assertCantRenameFile(shellFile1, shellFile2); |
| // app can't move shell file to its media directory. |
| assertCantRenameFile(shellFile1, mediaFile1); |
| // However, even without permissions, app can rename files in its own external media |
| // directory. |
| assertThat(mediaFile1.createNewFile()).isTrue(); |
| assertThat(mediaFile1.renameTo(mediaFile2)).isTrue(); |
| assertThat(mediaFile2.exists()).isTrue(); |
| } finally { |
| mediaFile1.delete(); |
| mediaFile2.delete(); |
| } |
| } |
| |
| /** |
| * Test that legacy app with no storage permission can only rename files in app external |
| * directories. |
| */ |
| @Test |
| public void testCantRename_noStoragePermission() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ false); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ false); |
| |
| final File shellFile1 = new File(TestUtils.getExternalStorageDir(), |
| "LegacyAccessHostTest_shell"); |
| final File shellFile2 = new File(TestUtils.getDownloadDir(), "LegacyFileAccessTest_shell"); |
| final File mediaFile1 = |
| new File(TestUtils.getExternalMediaDir(), |
| "LegacyFileAccessTest1"); |
| final File mediaFile2 = |
| new File(TestUtils.getExternalMediaDir(), |
| "LegacyFileAccessTest2"); |
| try { |
| // app can't rename shell file. |
| assertCantRenameFile(shellFile1, shellFile2); |
| // app can't move shell file to its media directory. |
| assertCantRenameFile(shellFile1, mediaFile1); |
| // However, even without permissions, app can rename files in its own external media |
| // directory. |
| assertThat(mediaFile1.createNewFile()).isTrue(); |
| assertThat(mediaFile1.renameTo(mediaFile2)).isTrue(); |
| assertThat(mediaFile2.exists()).isTrue(); |
| } finally { |
| mediaFile1.delete(); |
| mediaFile2.delete(); |
| } |
| } |
| |
| /** |
| * b/156046098, Test that MediaProvider doesn't throw UNIQUE constraint error while updating db |
| * rows corresponding to renamed directory. |
| */ |
| @Test |
| public void testRenameDirectoryAndUpdateDB_hasW() throws Exception { |
| final String testDirectoryName = "LegacyFileAccessTestDirectory"; |
| File directoryOldPath = new File(TestUtils.getDcimDir(), testDirectoryName); |
| File directoryNewPath = new File(TestUtils.getMoviesDir(), testDirectoryName); |
| try { |
| if (directoryOldPath.exists()) { |
| executeShellCommand("rm -r " + directoryOldPath.getPath()); |
| } |
| assertThat(directoryOldPath.mkdirs()).isTrue(); |
| assertCanRenameDirectory(directoryOldPath, directoryNewPath, null, null); |
| |
| ContentValues values = new ContentValues(); |
| values.put(MediaStore.MediaColumns.DATA, directoryNewPath.getPath()); |
| // Verify that updating directoryOldPath to directoryNewPath doesn't throw |
| // UNIQUE constraint error. |
| getContentResolver().update(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), |
| values, /*where*/ MediaStore.MediaColumns.DATA + "=?", |
| /*whereArgs*/ new String[] {directoryOldPath.getPath()}); |
| } finally { |
| directoryOldPath.delete(); |
| directoryNewPath.delete(); |
| } |
| } |
| |
| /** |
| * Test that legacy app with WRITE_EXTERNAL_STORAGE can delete all files, and corresponding |
| * database entry is deleted on deleting the file. |
| */ |
| @Test |
| public void testCanDeleteAllFiles_hasRW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File videoFile = new File(TestUtils.getExternalStorageDir(), VIDEO_FILE_NAME); |
| final File otherAppPdfFile = new File(TestUtils.getDownloadDir(), NONMEDIA_FILE_NAME); |
| |
| try { |
| assertThat(videoFile.createNewFile()).isTrue(); |
| assertDirectoryContains(videoFile.getParentFile(), videoFile); |
| |
| assertThat(getFileRowIdFromDatabase(videoFile)).isNotEqualTo(-1); |
| // Legacy app can delete its own file. |
| assertThat(videoFile.delete()).isTrue(); |
| // Deleting the file will remove videoFile entry from database. |
| assertThat(getFileRowIdFromDatabase(videoFile)).isEqualTo(-1); |
| |
| installApp(TEST_APP_A, false); |
| assertThat(createFileAs(TEST_APP_A, otherAppPdfFile.getAbsolutePath())).isTrue(); |
| assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isNotEqualTo(-1); |
| // Legacy app with write permission can delete the pdfFile owned by TestApp. |
| assertThat(otherAppPdfFile.delete()).isTrue(); |
| // Deleting the pdfFile also removes pdfFile from database. |
| assertThat(getFileRowIdFromDatabase(otherAppPdfFile)).isEqualTo(-1); |
| } finally { |
| deleteFileAsNoThrow(TEST_APP_A, otherAppPdfFile.getAbsolutePath()); |
| uninstallApp(TEST_APP_A); |
| videoFile.delete(); |
| } |
| } |
| |
| /** |
| * Test that file created by legacy app is inserted to MediaProvider database. And, |
| * MediaColumns.OWNER_PACKAGE_NAME is updated with calling package's name. |
| */ |
| @Test |
| public void testLegacyAppCanOwnAFile_hasW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File videoFile = new File(TestUtils.getExternalStorageDir(), VIDEO_FILE_NAME); |
| try { |
| assertThat(videoFile.createNewFile()).isTrue(); |
| |
| installApp(TEST_APP_A, true); |
| // videoFile is inserted to database, non-legacy app can see this videoFile on 'ls'. |
| assertThat(listAs(TEST_APP_A, TestUtils.getExternalStorageDir().getAbsolutePath())) |
| .contains(VIDEO_FILE_NAME); |
| |
| // videoFile is in database, row ID for videoFile can not be -1. |
| assertNotEquals(-1, getFileRowIdFromDatabase(videoFile)); |
| assertEquals(THIS_PACKAGE_NAME, getFileOwnerPackageFromDatabase(videoFile)); |
| |
| assertTrue(videoFile.delete()); |
| // videoFile is removed from database on delete, hence row ID is -1. |
| assertEquals(-1, getFileRowIdFromDatabase(videoFile)); |
| } finally { |
| videoFile.delete(); |
| uninstallApp(TEST_APP_A); |
| } |
| } |
| |
| @Test |
| public void testCreateAndRenameDoesntLeaveStaleDBRow_hasRW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File videoFile = new File(TestUtils.getDcimDir(), VIDEO_FILE_NAME); |
| final File renamedVideoFile = new File(TestUtils.getDcimDir(), "Renamed_" + VIDEO_FILE_NAME); |
| final ContentResolver cr = getContentResolver(); |
| |
| try { |
| assertThat(videoFile.createNewFile()).isTrue(); |
| assertThat(videoFile.renameTo(renamedVideoFile)).isTrue(); |
| |
| ContentValues values = new ContentValues(); |
| values.put(MediaStore.MediaColumns.DATA, renamedVideoFile.getAbsolutePath()); |
| // Insert new renamedVideoFile to database |
| final Uri uri = cr.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values, null); |
| assertNotNull(uri); |
| |
| // Query for all images/videos in the device. |
| // This shouldn't list videoFile which was renamed to renamedVideoFile. |
| final ArrayList<String> imageAndVideoFiles = getImageAndVideoFilesFromDatabase(); |
| assertThat(imageAndVideoFiles).contains(renamedVideoFile.getName()); |
| assertThat(imageAndVideoFiles).doesNotContain(videoFile.getName()); |
| } finally { |
| videoFile.delete(); |
| renamedVideoFile.delete(); |
| } |
| } |
| |
| /** |
| * b/150147690,b/150193381: Test that file rename doesn't delete any existing Uri. |
| */ |
| @Test |
| public void testRenameDoesntInvalidateUri_hasRW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File imageFile = new File(TestUtils.getDcimDir(), IMAGE_FILE_NAME); |
| final File temporaryImageFile = new File(TestUtils.getDcimDir(), IMAGE_FILE_NAME + "_.tmp"); |
| final ContentResolver cr = getContentResolver(); |
| |
| try { |
| assertThat(imageFile.createNewFile()).isTrue(); |
| try (final FileOutputStream fos = new FileOutputStream(imageFile)) { |
| fos.write(BYTES_DATA1); |
| } |
| // Insert this file to database. |
| ContentValues values = new ContentValues(); |
| values.put(MediaStore.MediaColumns.DATA, imageFile.getAbsolutePath()); |
| final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null); |
| assertNotNull(uri); |
| |
| Files.copy(imageFile, temporaryImageFile); |
| // Write more bytes to temporaryImageFile |
| try (final FileOutputStream fos = new FileOutputStream(temporaryImageFile, true)) { |
| fos.write(BYTES_DATA2); |
| } |
| assertThat(imageFile.delete()).isTrue(); |
| temporaryImageFile.renameTo(imageFile); |
| |
| // Previous uri of imageFile is unaltered after delete & rename. |
| final Uri scannedUri = MediaStore.scanFile(cr, imageFile); |
| assertThat(scannedUri.getLastPathSegment()).isEqualTo(uri.getLastPathSegment()); |
| |
| final byte[] expected = (STR_DATA1 + STR_DATA2).getBytes(); |
| assertFileContent(imageFile, expected); |
| } finally { |
| imageFile.delete(); |
| temporaryImageFile.delete(); |
| } |
| } |
| |
| /** |
| * b/150498564,b/150274099: Test that apps can rename files that are not in database. |
| */ |
| @Test |
| public void testCanRenameAFileWithNoDBRow_hasRW() throws Exception { |
| pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true); |
| pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true); |
| |
| final File directoryNoMedia = new File(TestUtils.getDcimDir(), ".directoryNoMedia"); |
| final File imageInNoMediaDir = new File(directoryNoMedia, IMAGE_FILE_NAME); |
| final File renamedImageInDCIM = new File(TestUtils.getDcimDir(), IMAGE_FILE_NAME); |
| final File noMediaFile = new File(directoryNoMedia, ".nomedia"); |
| final ContentResolver cr = getContentResolver(); |
| |
| try { |
| if (!directoryNoMedia.exists()) { |
| assertThat(directoryNoMedia.mkdirs()).isTrue(); |
| } |
| assertThat(noMediaFile.createNewFile()).isTrue(); |
| assertThat(imageInNoMediaDir.createNewFile()).isTrue(); |
| // Remove imageInNoMediaDir from database. |
| MediaStore.scanFile(cr, directoryNoMedia); |
| |
| // Query for all images/videos in the device. This shouldn't list imageInNoMediaDir |
| assertThat(getImageAndVideoFilesFromDatabase()) |
| .doesNotContain(imageInNoMediaDir.getName()); |
| |
| // Rename shouldn't throw error even if imageInNoMediaDir is not in database. |
| assertThat(imageInNoMediaDir.renameTo(renamedImageInDCIM)).isTrue(); |
| // We can insert renamedImageInDCIM to database |
| ContentValues values = new ContentValues(); |
| values.put(MediaStore.MediaColumns.DATA, renamedImageInDCIM.getAbsolutePath()); |
| final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, null); |
| assertNotNull(uri); |
| } finally { |
| imageInNoMediaDir.delete(); |
| renamedImageInDCIM.delete(); |
| noMediaFile.delete(); |
| directoryNoMedia.delete(); |
| } |
| } |
| |
| private static void assertCanCreateFile(File file) throws IOException { |
| if (file.exists()) { |
| file.delete(); |
| } |
| try { |
| if (!file.createNewFile()) { |
| fail("Could not create file: " + file); |
| } |
| } finally { |
| file.delete(); |
| } |
| } |
| |
| private static void assertCanCreateDir(File dir) throws IOException { |
| if (dir.exists()) { |
| if (!dir.delete()) { |
| Log.w(TAG, |
| "Can't create dir " + dir + " because it already exists and we can't " |
| + "delete it!"); |
| return; |
| } |
| } |
| try { |
| if (!dir.mkdir()) { |
| fail("Could not mkdir: " + dir); |
| } |
| } finally { |
| dir.delete(); |
| } |
| } |
| |
| /** |
| * Queries {@link ContentResolver} for all image and video files, returns display name of |
| * corresponding files. |
| */ |
| private static ArrayList<String> getImageAndVideoFilesFromDatabase() { |
| ArrayList<String> mediaFiles = new ArrayList<>(); |
| final String selection = "is_pending = 0 AND is_trashed = 0 AND " |
| + "(media_type = ? OR media_type = ?)"; |
| final String[] selectionArgs = |
| new String[] {String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), |
| String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)}; |
| |
| try (Cursor c = getContentResolver().query( |
| MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), |
| /* projection */ new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, |
| selection, selectionArgs, null)) { |
| while (c.moveToNext()) { |
| mediaFiles.add(c.getString(0)); |
| } |
| } |
| return mediaFiles; |
| } |
| } |