/* * Copyright (C) 2018 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 com.android.providers.media; import static com.android.providers.media.scan.MediaScannerTest.stage; import static com.android.providers.media.util.FileUtils.extractDisplayName; import static com.android.providers.media.util.FileUtils.extractRelativePath; import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory; import static com.android.providers.media.util.FileUtils.isDownload; import static com.android.providers.media.util.FileUtils.isDownloadDir; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.Manifest; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Environment; import android.os.UserHandle; import android.provider.MediaStore; import android.provider.MediaStore.Audio.AudioColumns; import android.provider.MediaStore.Files.FileColumns; import android.provider.MediaStore.Images.ImageColumns; import android.provider.MediaStore.MediaColumns; import android.util.ArrayMap; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.providers.media.MediaProvider.FallbackException; import com.android.providers.media.MediaProvider.VolumeArgumentException; import com.android.providers.media.MediaProvider.VolumeNotFoundException; import com.android.providers.media.scan.MediaScannerTest.IsolatedContext; import com.android.providers.media.util.FileUtils; import com.android.providers.media.util.FileUtilsTest; import com.android.providers.media.util.SQLiteQueryBuilder; import org.junit.AfterClass; import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintWriter; import java.sql.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @RunWith(AndroidJUnit4.class) public class MediaProviderTest { static final String TAG = "MediaProviderTest"; // The test app without permissions static final String PERMISSIONLESS_APP = "com.android.providers.media.testapp.withoutperms"; private static Context sIsolatedContext; private static ContentResolver sIsolatedResolver; @BeforeClass public static void setUp() { InstrumentationRegistry.getInstrumentation().getUiAutomation() .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, Manifest.permission.READ_COMPAT_CHANGE_CONFIG, Manifest.permission.READ_DEVICE_CONFIG, Manifest.permission.INTERACT_ACROSS_USERS); final Context context = InstrumentationRegistry.getTargetContext(); sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); sIsolatedResolver = sIsolatedContext.getContentResolver(); } @AfterClass public static void tearDown() { InstrumentationRegistry.getInstrumentation() .getUiAutomation().dropShellPermissionIdentity(); } /** * To fully exercise all our tests, we require that the Cuttlefish emulator * have both emulated primary storage and an SD card be present. */ @Test public void testCuttlefish() { Assume.assumeTrue(Build.MODEL.contains("Cuttlefish")); assertTrue("Cuttlefish must have both emulated storage and an SD card to exercise tests", MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext()) .size() > 1); } @Test public void testSchema() { for (String path : new String[] { "images/media", "images/media/1", "images/thumbnails", "images/thumbnails/1", "audio/media", "audio/media/1", "audio/media/1/genres", "audio/media/1/genres/1", "audio/genres", "audio/genres/1", "audio/genres/1/members", "audio/playlists", "audio/playlists/1", "audio/playlists/1/members", "audio/playlists/1/members/1", "audio/artists", "audio/artists/1", "audio/artists/1/albums", "audio/albums", "audio/albums/1", "audio/albumart", "audio/albumart/1", "video/media", "video/media/1", "video/thumbnails", "video/thumbnails/1", "file", "file/1", "downloads", "downloads/1", }) { final Uri probe = MediaStore.AUTHORITY_URI.buildUpon() .appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build(); try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) { assertNotNull("probe", c); } try { sIsolatedResolver.getType(probe); } catch (IllegalStateException tolerated) { } } } @Test public void testLocale() { try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { ((MediaProvider) cpc.getLocalContentProvider()) .onLocaleChanged(); } } @Test public void testDump() throws Exception { try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { cpc.getLocalContentProvider().dump(null, new PrintWriter(new ByteArrayOutputStream()), null); } } /** * Verify that our fallback exceptions throw on modern apps while degrading * gracefully for legacy apps. */ @Test public void testFallbackException() throws Exception { for (FallbackException e : new FallbackException[] { new FallbackException("test", Build.VERSION_CODES.Q), new VolumeNotFoundException("test"), new VolumeArgumentException(new File("/"), Collections.emptyList()) }) { // Modern apps should get thrown assertThrows(Exception.class, () -> { e.translateForInsert(Build.VERSION_CODES.CUR_DEVELOPMENT); }); assertThrows(Exception.class, () -> { e.translateForUpdateDelete(Build.VERSION_CODES.CUR_DEVELOPMENT); }); assertThrows(Exception.class, () -> { e.translateForQuery(Build.VERSION_CODES.CUR_DEVELOPMENT); }); // Legacy apps gracefully log without throwing assertEquals(null, e.translateForInsert(Build.VERSION_CODES.BASE)); assertEquals(0, e.translateForUpdateDelete(Build.VERSION_CODES.BASE)); assertEquals(null, e.translateForQuery(Build.VERSION_CODES.BASE)); } } /** * We already have solid coverage of this logic in {@link IdleServiceTest}, * but the coverage system currently doesn't measure that, so we add the * bare minimum local testing here to convince the tooling that it's * covered. */ @Test public void testIdle() throws Exception { try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { ((MediaProvider) cpc.getLocalContentProvider()) .onIdleMaintenance(new CancellationSignal()); } } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testCanonicalize() throws Exception { // We might have old files lurking, so force a clean slate final Context context = InstrumentationRegistry.getTargetContext(); sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); sIsolatedResolver = sIsolatedContext.getContentResolver(); final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); for (File file : new File[] { stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")), stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")), stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg")) }) { final Uri uri = MediaStore.scanFile(sIsolatedResolver, file); Log.v(TAG, "Scanned " + file + " as " + uri); final Uri forward = sIsolatedResolver.canonicalize(uri); final Uri reverse = sIsolatedResolver.uncanonicalize(forward); assertEquals(ContentUris.parseId(uri), ContentUris.parseId(forward)); assertEquals(ContentUris.parseId(uri), ContentUris.parseId(reverse)); } } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testMetadata() { assertNotNull(MediaStore.getVersion(sIsolatedContext, MediaStore.VOLUME_EXTERNAL_PRIMARY)); assertNotNull(MediaStore.getGeneration(sIsolatedResolver, MediaStore.VOLUME_EXTERNAL_PRIMARY)); } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testCreateRequest() throws Exception { final Collection uris = Arrays.asList( MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)); assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris)); } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testCheckUriPermission() throws Exception { final ContentValues values = new ContentValues(); values.put(MediaColumns.DISPLAY_NAME, "test.mp3"); values.put(MediaColumns.MIME_TYPE, "audio/mpeg"); final Uri uri = sIsolatedResolver.insert( MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values); assertEquals(PackageManager.PERMISSION_GRANTED, sIsolatedResolver.checkUriPermission(uri, android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)); } @Test public void testTrashLongFileNameItemHasTrimmedFileName() throws Exception { testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_TRASHED); } @Test public void testPendingLongFileNameItemHasTrimmedFileName() throws Exception { testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_PENDING); } private void testActionLongFileNameItemHasTrimmedFileName(String columnKey) throws Exception { // We might have old files lurking, so force a clean slate final Context context = InstrumentationRegistry.getTargetContext(); sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); sIsolatedResolver = sIsolatedContext.getContentResolver(); final String[] projection = new String[]{MediaColumns.DATA}; final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // create extreme long file name final String originalName = FileUtilsTest.createExtremeFileName("test" + System.nanoTime(), ".jpg"); File file = stage(R.raw.lg_g4_iso_800_jpg, new File(dir, originalName)); final Uri uri = MediaStore.scanFile(sIsolatedResolver, file); Log.v(TAG, "Scanned " + file + " as " + uri); try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { assertNotNull(c); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); final String data = c.getString(0); final String result = FileUtils.extractDisplayName(data); assertEquals(originalName, result); } final Bundle extras = new Bundle(); extras.putBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true); final ContentValues values = new ContentValues(); values.put(columnKey, 1); sIsolatedResolver.update(uri, values, extras); try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { assertNotNull(c); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); final String data = c.getString(0); final String result = FileUtils.extractDisplayName(data); assertThat(result.length()).isAtMost(FileUtilsTest.MAX_FILENAME_BYTES); assertNotEquals(originalName, result); } } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testBulkInsert() throws Exception { final ContentValues values1 = new ContentValues(); values1.put(MediaColumns.DISPLAY_NAME, "test1.mp3"); values1.put(MediaColumns.MIME_TYPE, "audio/mpeg"); final ContentValues values2 = new ContentValues(); values2.put(MediaColumns.DISPLAY_NAME, "test2.mp3"); values2.put(MediaColumns.MIME_TYPE, "audio/mpeg"); final Uri targetUri = MediaStore.Audio.Media .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEquals(2, sIsolatedResolver.bulkInsert(targetUri, new ContentValues[] { values1, values2 })); } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testCustomCollator() throws Exception { final Bundle extras = new Bundle(); extras.putString(ContentResolver.QUERY_ARG_SORT_LOCALE, "en"); try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI, null, extras, null)) { assertNotNull(c); } } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testGetRedactionRanges_Image() throws Exception { final File file = File.createTempFile("test", ".jpg"); stage(R.raw.test_image, file); assertNotNull(MediaProvider.getRedactionRanges(file)); } /** * We already have solid coverage of this logic in * {@code CtsProviderTestCases}, but the coverage system currently doesn't * measure that, so we add the bare minimum local testing here to convince * the tooling that it's covered. */ @Test public void testGetRedactionRanges_Video() throws Exception { final File file = File.createTempFile("test", ".mp4"); stage(R.raw.test_video, file); assertNotNull(MediaProvider.getRedactionRanges(file)); } @Test public void testComputeCommonPrefix_Single() { assertEquals(Uri.parse("content://authority/1/2/3"), MediaProvider.computeCommonPrefix(Arrays.asList( Uri.parse("content://authority/1/2/3")))); } @Test public void testComputeCommonPrefix_Deeper() { assertEquals(Uri.parse("content://authority/1/2/3"), MediaProvider.computeCommonPrefix(Arrays.asList( Uri.parse("content://authority/1/2/3/4"), Uri.parse("content://authority/1/2/3/4/5"), Uri.parse("content://authority/1/2/3")))); } @Test public void testComputeCommonPrefix_Siblings() { assertEquals(Uri.parse("content://authority/1/2"), MediaProvider.computeCommonPrefix(Arrays.asList( Uri.parse("content://authority/1/2/3"), Uri.parse("content://authority/1/2/99")))); } @Test public void testComputeCommonPrefix_Drastic() { assertEquals(Uri.parse("content://authority"), MediaProvider.computeCommonPrefix(Arrays.asList( Uri.parse("content://authority/1/2/3"), Uri.parse("content://authority/99/99/99")))); } private static String getPathOwnerPackageName(String path) { return FileUtils.extractPathOwnerPackageName(path); } @Test public void testPathOwnerPackageName_None() throws Exception { assertEquals(null, getPathOwnerPackageName(null)); assertEquals(null, getPathOwnerPackageName("/data/path")); } @Test public void testPathOwnerPackageName_Emulated() throws Exception { assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/DCIM/foo.jpg")); assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/")); assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/data/")); assertEquals("com.example", getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/")); assertEquals("com.example", getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/foo.jpg")); assertEquals("com.example", getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg")); assertEquals("com.example", getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg")); } @Test public void testPathOwnerPackageName_Portable() throws Exception { assertEquals(null, getPathOwnerPackageName("/storage/0000-0000/DCIM/foo.jpg")); assertEquals("com.example", getPathOwnerPackageName("/storage/0000-0000/Android/data/com.example/foo.jpg")); } @Test public void testBuildData_Simple() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Pictures/file.png", buildFile(uri, null, "file", "image/png")); assertEndsWith("/Pictures/file.png", buildFile(uri, null, "file.png", "image/png")); assertEndsWith("/Pictures/file.jpg.png", buildFile(uri, null, "file.jpg", "image/png")); } @Test public void testBuildData_withUserId() throws Exception { final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues values = new ContentValues(); values.put(MediaColumns.DISPLAY_NAME, "test_userid"); values.put(MediaColumns.MIME_TYPE, "image/png"); Uri result = sIsolatedResolver.insert(uri, values); try (Cursor c = sIsolatedResolver.query(result, new String[]{MediaColumns.DISPLAY_NAME, FileColumns._USER_ID}, null, null)) { assertNotNull(c); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); assertEquals("test_userid.png", c.getString(0)); assertEquals(UserHandle.myUserId(), c.getInt(1)); } } @Test public void testBuildData_Primary() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/DCIM/IMG_1024.JPG", buildFile(uri, Environment.DIRECTORY_DCIM, "IMG_1024.JPG", "image/jpeg")); } @Test @Ignore("Enable as part of b/142561358") public void testBuildData_Secondary() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Pictures/Screenshots/foo.png", buildFile(uri, "Pictures/Screenshots", "foo.png", "image/png")); } @Test public void testBuildData_InvalidNames() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Pictures/foo_bar.png", buildFile(uri, null, "foo/bar", "image/png")); assertEndsWith("/Pictures/_.hidden.png", buildFile(uri, null, ".hidden", "image/png")); } @Test public void testBuildData_InvalidTypes() throws Exception { for (String type : new String[] { "audio/foo", "video/foo", "image/foo", "application/foo", "foo/foo" }) { if (!type.startsWith("audio/")) { assertThrows(IllegalArgumentException.class, () -> { buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo", type); }); } if (!type.startsWith("video/")) { assertThrows(IllegalArgumentException.class, () -> { buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo", type); }); } if (!type.startsWith("image/")) { assertThrows(IllegalArgumentException.class, () -> { buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo", type); }); } } } @Test public void testBuildData_InvalidSecondaryTypes() throws Exception { assertEndsWith("/Pictures/foo.png", buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo.png", "image/*")); assertThrows(IllegalArgumentException.class, () -> { buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo", "video/*"); }); assertThrows(IllegalArgumentException.class, () -> { buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), null, "foo.mp4", "audio/*"); }); } @Test public void testBuildData_EmptyTypes() throws Exception { Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Pictures/foo.png", buildFile(uri, null, "foo.png", "")); uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith(".mp4", buildFile(uri, null, "", "")); } @Test public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception { final MediaProvider provider = new MediaProvider() { @Override public boolean isFuseThread() { return false; } @Override public int getCallingPackageTargetSdkVersion() { return Build.VERSION_CODES.Q; } }; final ProviderInfo info = sIsolatedContext.getPackageManager() .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA); // Attach providerInfo, to make sure mCallingIdentity can be populated provider.attachInfo(sIsolatedContext, info); final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues values = new ContentValues(); values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); provider.ensureFileColumns(uri, values); assertMimetype(values, "image/jpeg"); assertDisplayName(values, "pngimage.png.jpg"); values.clear(); values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); values.put(MediaColumns.MIME_TYPE, ""); provider.ensureFileColumns(uri, values); assertMimetype(values, "image/jpeg"); assertDisplayName(values, "pngimage.png.jpg"); values.clear(); values.put(MediaColumns.MIME_TYPE, ""); provider.ensureFileColumns(uri, values); assertMimetype(values, "image/jpeg"); values.clear(); values.put(MediaColumns.DISPLAY_NAME, "foo.foo"); provider.ensureFileColumns(uri, values); assertMimetype(values, "image/jpeg"); assertDisplayName(values, "foo.foo.jpg"); } @Ignore("Enable as part of b/142561358") public void testBuildData_Charset() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Pictures/foo__bar/bar__baz.png", buildFile(uri, "Pictures/foo\0\0bar", "bar::baz.png", "image/png")); } @Test public void testBuildData_Playlists() throws Exception { final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Music/my_playlist.m3u", buildFile(uri, null, "my_playlist", "audio/mpegurl")); assertEndsWith("/Movies/my_playlist.pls", buildFile(uri, "Movies", "my_playlist", "audio/x-scpls")); } @Test public void testBuildData_Subtitles() throws Exception { final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Movies/my_subtitle.srt", buildFile(uri, null, "my_subtitle", "application/x-subrip")); assertEndsWith("/Music/my_lyrics.lrc", buildFile(uri, "Music", "my_lyrics", "application/lrc")); } @Test public void testBuildData_Downloads() throws Exception { final Uri uri = MediaStore.Downloads .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); assertEndsWith("/Download/linux.iso", buildFile(uri, null, "linux.iso", "application/x-iso9660-image")); } @Test public void testBuildData_Pending_FromValues() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues forward = new ContentValues(); forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); forward.put(MediaColumns.IS_PENDING, 1); forward.put(MediaColumns.IS_TRASHED, 0); forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); ensureFileColumns(uri, forward); // Requested filename remains intact, but raw path on disk is mutated to // reflect that it's a pending item with a specific expiration time assertEquals("IMG1024.JPG", forward.getAsString(MediaColumns.DISPLAY_NAME)); assertEndsWith(".pending-1577836800-IMG1024.JPG", forward.getAsString(MediaColumns.DATA)); } @Test public void testBuildData_Pending_FromValues_differentLocale() throws Exception { // See b/174120008 for context. Locale defaultLocale = Locale.getDefault(); try { Locale.setDefault(new Locale("ar", "SA")); testBuildData_Pending_FromValues(); } finally { Locale.setDefault(defaultLocale); } } @Test public void testBuildData_Pending_FromData() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues reverse = new ContentValues(); reverse.put(MediaColumns.DATA, "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG"); ensureFileColumns(uri, reverse); assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); } @Test public void testBuildData_Trashed_FromValues() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues forward = new ContentValues(); forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); forward.put(MediaColumns.IS_PENDING, 0); forward.put(MediaColumns.IS_TRASHED, 1); forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); ensureFileColumns(uri, forward); // Requested filename remains intact, but raw path on disk is mutated to // reflect that it's a trashed item with a specific expiration time assertEquals("IMG1024.JPG", forward.getAsString(MediaColumns.DISPLAY_NAME)); assertEndsWith(".trashed-1577836800-IMG1024.JPG", forward.getAsString(MediaColumns.DATA)); } @Test public void testBuildData_Trashed_FromValues_differentLocale() throws Exception { // See b/174120008 for context. Locale defaultLocale = Locale.getDefault(); try { Locale.setDefault(new Locale("ar", "SA")); testBuildData_Trashed_FromValues(); } finally { Locale.setDefault(defaultLocale); } } @Test public void testBuildData_Trashed_FromData() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues reverse = new ContentValues(); reverse.put(MediaColumns.DATA, "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG"); ensureFileColumns(uri, reverse); assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); } @Test public void testGreylist() throws Exception { assertFalse(isGreylistMatch( "SELECT secret FROM other_table")); assertTrue(isGreylistMatch( "case when case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end > case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end then case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end else case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end end as corrected_added_modified")); assertTrue(isGreylistMatch( "MAX(case when (datetaken >= 157680000 and datetaken < 1892160000) then datetaken * 1000 when (datetaken >= 157680000000 and datetaken < 1892160000000) then datetaken when (datetaken >= 157680000000000 and datetaken < 1892160000000000) then datetaken / 1000 else 0 end)")); assertTrue(isGreylistMatch( "0 as orientation")); assertTrue(isGreylistMatch( "\"content://media/internal/audio/media\"")); } @Test public void testGreylist_115845887() { assertTrue(isGreylistMatch( "MAX(*)")); assertTrue(isGreylistMatch( "MAX(_id)")); assertTrue(isGreylistMatch( "sum(column_name)")); assertFalse(isGreylistMatch( "SUM(foo+bar)")); assertTrue(isGreylistMatch( "count(column_name)")); assertFalse(isGreylistMatch( "count(other_table.column_name)")); } @Test public void testGreylist_116489751_116135586_116117120_116084561_116074030_116062802() { assertTrue(isGreylistMatch( "MAX(case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end)")); } @Test public void testGreylist_116699470() { assertTrue(isGreylistMatch( "MAX(case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end)")); } @Test public void testGreylist_116531759() { assertTrue(isGreylistMatch( "count(*)")); assertTrue(isGreylistMatch( "COUNT(*)")); assertFalse(isGreylistMatch( "xCOUNT(*)")); assertTrue(isGreylistMatch( "count(*) AS image_count")); assertTrue(isGreylistMatch( "count(_id)")); assertTrue(isGreylistMatch( "count(_id) AS image_count")); assertTrue(isGreylistMatch( "column_a AS column_b")); assertFalse(isGreylistMatch( "other_table.column_a AS column_b")); } @Test public void testGreylist_118475754() { assertTrue(isGreylistMatch( "count(*) pcount")); assertTrue(isGreylistMatch( "foo AS bar")); assertTrue(isGreylistMatch( "foo bar")); assertTrue(isGreylistMatch( "count(foo) AS bar")); assertTrue(isGreylistMatch( "count(foo) bar")); } @Test public void testGreylist_119522660() { assertTrue(isGreylistMatch( "CAST(_id AS TEXT) AS string_id")); assertTrue(isGreylistMatch( "cast(_id as text)")); } @Test public void testGreylist_126945991() { assertTrue(isGreylistMatch( "substr(_data, length(_data)-length(_display_name), 1) as filename_prevchar")); } @Test public void testGreylist_127900881() { assertTrue(isGreylistMatch( "*")); } @Test public void testGreylist_128389972() { assertTrue(isGreylistMatch( " count(bucket_id) images_count")); } @Test public void testGreylist_129746861() { assertTrue(isGreylistMatch( "case when (datetaken >= 157680000 and datetaken < 1892160000) then datetaken * 1000 when (datetaken >= 157680000000 and datetaken < 1892160000000) then datetaken when (datetaken >= 157680000000000 and datetaken < 1892160000000000) then datetaken / 1000 else 0 end")); } @Test public void testGreylist_114112523() { assertTrue(isGreylistMatch( "audio._id AS _id")); } @Test public void testComputeProjection_AggregationAllowed() throws Exception { final SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); final ArrayMap map = new ArrayMap<>(); map.put("external", "internal"); builder.setProjectionMap(map); builder.setStrict(true); builder.setStrictColumns(true); assertArrayEquals( new String[] { "internal" }, builder.computeProjection(null)); assertArrayEquals( new String[] { "internal" }, builder.computeProjection(new String[] { "external" })); assertThrows(IllegalArgumentException.class, () -> { builder.computeProjection(new String[] { "internal" }); }); assertThrows(IllegalArgumentException.class, () -> { builder.computeProjection(new String[] { "MIN(internal)" }); }); assertArrayEquals( new String[] { "MIN(internal)" }, builder.computeProjection(new String[] { "MIN(external)" })); assertThrows(IllegalArgumentException.class, () -> { builder.computeProjection(new String[] { "FOO(external)" }); }); } @Test public void testIsDownload() throws Exception { assertTrue(isDownload("/storage/emulated/0/Download/colors.png")); assertTrue(isDownload("/storage/emulated/0/Download/test.pdf")); assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4")); assertTrue(isDownload("/storage/0000-0000/Download/foo.txt")); assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png")); assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png")); assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt")); assertFalse(isDownload("/storage/emulated/0/Download")); } @Test public void testIsDownloadDir() throws Exception { assertTrue(isDownloadDir("/storage/emulated/0/Download")); assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png")); assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/")); } @Test public void testComputeDataValues_Grouped() throws Exception { for (String data : new String[] { "/storage/0000-0000/DCIM/Camera/IMG1024.JPG", "/storage/0000-0000/DCIM/Camera/iMg1024.JpG", "/storage/0000-0000/DCIM/Camera/IMG1024.CR2", "/storage/0000-0000/DCIM/Camera/IMG1024.BURST001.JPG", }) { final ContentValues values = computeDataValues(data); assertVolume(values, "0000-0000"); assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); assertRelativePath(values, "DCIM/Camera/"); } } @Test public void testComputeDataValues_Extensions() throws Exception { ContentValues values; values = computeDataValues("/storage/0000-0000/DCIM/Camera/IMG1024"); assertVolume(values, "0000-0000"); assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); assertRelativePath(values, "DCIM/Camera/"); values = computeDataValues("/storage/0000-0000/DCIM/Camera/.foo"); assertVolume(values, "0000-0000"); assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); assertRelativePath(values, "DCIM/Camera/"); values = computeDataValues("/storage/476A-17F8/123456/test.png"); assertVolume(values, "476a-17f8"); assertBucket(values, "/storage/476A-17F8/123456", "123456"); assertRelativePath(values, "123456/"); values = computeDataValues("/storage/476A-17F8/123456/789/test.mp3"); assertVolume(values, "476a-17f8"); assertBucket(values, "/storage/476A-17F8/123456/789", "789"); assertRelativePath(values, "123456/789/"); } @Test public void testComputeDataValues_DirectoriesInvalid() throws Exception { for (String data : new String[] { "/storage/IMG1024.JPG", "/data/media/IMG1024.JPG", "IMG1024.JPG", }) { final ContentValues values = computeDataValues(data); assertRelativePath(values, null); } } @Test public void testComputeDataValues_Directories() throws Exception { ContentValues values; for (String top : new String[] { "/storage/emulated/0", }) { values = computeDataValues(top + "/IMG1024.JPG"); assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); assertBucket(values, top, null); assertRelativePath(values, "/"); values = computeDataValues(top + "/One/IMG1024.JPG"); assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); assertBucket(values, top + "/One", "One"); assertRelativePath(values, "One/"); values = computeDataValues(top + "/One/Two/IMG1024.JPG"); assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); assertBucket(values, top + "/One/Two", "Two"); assertRelativePath(values, "One/Two/"); values = computeDataValues(top + "/One/Two/Three/IMG1024.JPG"); assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); assertBucket(values, top + "/One/Two/Three", "Three"); assertRelativePath(values, "One/Two/Three/"); } } @Test public void testEnsureFileColumns_resolvesMimeType() throws Exception { final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final ContentValues values = new ContentValues(); values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); final MediaProvider provider = new MediaProvider() { @Override public boolean isFuseThread() { return false; } @Override public int getCallingPackageTargetSdkVersion() { return Build.VERSION_CODES.CUR_DEVELOPMENT; } }; final ProviderInfo info = sIsolatedContext.getPackageManager() .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA); // Attach providerInfo, to make sure mCallingIdentity can be populated provider.attachInfo(sIsolatedContext, info); provider.ensureFileColumns(uri, values); assertMimetype(values, "image/png"); } @Test public void testRelativePathForInvalidDirectories() throws Exception { for (String path : new String[] { "/storage/emulated", "/storage", "/data/media/Foo.jpg", "Foo.jpg", "storage/Foo" }) { assertEquals(null, FileUtils.extractRelativePathForDirectory(path)); } } @Test public void testRelativePathForValidDirectories() throws Exception { for (String prefix : new String[] { "/storage/emulated/0", "/storage/emulated/10", "/storage/ABCD-1234" }) { assertRelativePathForDirectory(prefix, "/"); assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/"); assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/"); assertRelativePathForDirectory(prefix + "/Z", "Z/"); assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo", "Android/media/com.example/Foo/"); } } @Test public void testComputeAudioKeyValues_167339595_differentAlbumIds() throws Exception { // same album name, different album artists final ContentValues valuesOne = new ContentValues(); valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3"); valuesOne.put(AudioColumns.TITLE, "Clocks"); valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood"); valuesOne.put(AudioColumns.ALBUM_ARTIST, "Coldplay"); valuesOne.put(AudioColumns.GENRE, "Rock"); valuesOne.put(AudioColumns.IS_MUSIC, true); final ContentValues valuesTwo = new ContentValues(); valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3"); valuesTwo.put(AudioColumns.TITLE, "Sounds"); valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood"); valuesTwo.put(AudioColumns.ALBUM_ARTIST, "ColdplayTwo"); valuesTwo.put(AudioColumns.GENRE, "Alternative rock"); valuesTwo.put(AudioColumns.IS_MUSIC, true); MediaProvider.computeAudioKeyValues(valuesOne); final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID); MediaProvider.computeAudioKeyValues(valuesTwo); final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID); assertNotEquals(albumIdOne, albumIdTwo); // same album name, different paths, no album artists final ContentValues valuesThree = new ContentValues(); valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3"); valuesThree.put(AudioColumns.TITLE, "Silent"); valuesThree.put(AudioColumns.ALBUM, "Rainbow"); valuesThree.put(AudioColumns.ARTIST, "Sample1"); valuesThree.put(AudioColumns.GENRE, "Rock"); valuesThree.put(AudioColumns.IS_MUSIC, true); final ContentValues valuesFour = new ContentValues(); valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesFour.put(FileColumns.DATA, "/storage/emulated/0/123456/Rainbow.mp3"); valuesFour.put(AudioColumns.TITLE, "Rainbow"); valuesFour.put(AudioColumns.ALBUM, "Rainbow"); valuesFour.put(AudioColumns.ARTIST, "Sample2"); valuesFour.put(AudioColumns.GENRE, "Alternative rock"); valuesFour.put(AudioColumns.IS_MUSIC, true); MediaProvider.computeAudioKeyValues(valuesThree); final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID); MediaProvider.computeAudioKeyValues(valuesFour); final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID); assertNotEquals(albumIdThree, albumIdFour); } @Test public void testComputeAudioKeyValues_167339595_sameAlbumId() throws Exception { // same album name, same path, no album artists final ContentValues valuesOne = new ContentValues(); valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3"); valuesOne.put(AudioColumns.TITLE, "Clocks"); valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood"); valuesOne.put(AudioColumns.GENRE, "Rock"); valuesOne.put(AudioColumns.IS_MUSIC, true); final ContentValues valuesTwo = new ContentValues(); valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3"); valuesTwo.put(AudioColumns.TITLE, "Sounds"); valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood"); valuesTwo.put(AudioColumns.GENRE, "Alternative rock"); valuesTwo.put(AudioColumns.IS_MUSIC, true); MediaProvider.computeAudioKeyValues(valuesOne); final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID); MediaProvider.computeAudioKeyValues(valuesTwo); final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID); assertEquals(albumIdOne, albumIdTwo); // same album name, same album artists, different artists final ContentValues valuesThree = new ContentValues(); valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3"); valuesThree.put(AudioColumns.TITLE, "Silent"); valuesThree.put(AudioColumns.ALBUM, "Rainbow"); valuesThree.put(AudioColumns.ALBUM_ARTIST, "Various Artists"); valuesThree.put(AudioColumns.ARTIST, "Sample1"); valuesThree.put(AudioColumns.GENRE, "Rock"); valuesThree.put(AudioColumns.IS_MUSIC, true); final ContentValues valuesFour = new ContentValues(); valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); valuesFour.put(FileColumns.DATA, "/storage/emulated/0/Rainbow.mp3"); valuesFour.put(AudioColumns.TITLE, "Rainbow"); valuesFour.put(AudioColumns.ALBUM, "Rainbow"); valuesFour.put(AudioColumns.ALBUM_ARTIST, "Various Artists"); valuesFour.put(AudioColumns.ARTIST, "Sample2"); valuesFour.put(AudioColumns.GENRE, "Alternative rock"); valuesFour.put(AudioColumns.IS_MUSIC, true); MediaProvider.computeAudioKeyValues(valuesThree); final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID); MediaProvider.computeAudioKeyValues(valuesFour); final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID); assertEquals(albumIdThree, albumIdFour); } @Test public void testQueryAudioViewsNoTrashedItem() throws Exception { testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_TRASHED); } @Test public void testQueryAudioViewsNoPendingItem() throws Exception { testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_PENDING); } private void testQueryAudioViewsNoItemWithColumn(String columnKey) throws Exception { // We might have old files lurking, so force a clean slate final Context context = InstrumentationRegistry.getTargetContext(); sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); sIsolatedResolver = sIsolatedContext.getContentResolver(); final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); final File audio = new File(dir, "test" + System.nanoTime() + ".mp3"); final Uri audioUri = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); final String album = "TestAlbum" + System.nanoTime(); final String artist = "TestArtist" + System.nanoTime(); final String genre = "TestGenre" + System.nanoTime(); final String relativePath = extractRelativePath(audio.getAbsolutePath()); final String displayName = extractDisplayName(audio.getAbsolutePath()); ContentValues values = new ContentValues(); values.put(MediaStore.Audio.Media.ALBUM, album); values.put(MediaStore.Audio.Media.ARTIST, artist); values.put(MediaStore.Audio.Media.GENRE, genre); values.put(MediaStore.Audio.Media.DISPLAY_NAME, displayName); values.put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath); values.put(MediaStore.Audio.Media.IS_MUSIC, 1); values.put(columnKey, 1); Uri result = sIsolatedResolver.insert(audioUri, values); // Check the audio file is inserted correctly try (Cursor c = sIsolatedResolver.query(result, new String[]{MediaColumns.DISPLAY_NAME, columnKey}, null, null)) { assertNotNull(c); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); assertEquals(displayName, c.getString(0)); assertEquals(1, c.getInt(1)); } final String volume = MediaStore.VOLUME_EXTERNAL_PRIMARY; assertQueryResultNoItems(MediaStore.Audio.Albums.getContentUri(volume)); assertQueryResultNoItems(MediaStore.Audio.Artists.getContentUri(volume)); assertQueryResultNoItems(MediaStore.Audio.Genres.getContentUri(volume)); } private static void assertQueryResultNoItems(Uri uri) throws Exception { try (Cursor c = sIsolatedResolver.query(uri, null, null, null, null)) { assertNotNull(c); assertEquals(0, c.getCount()); } } private static void assertRelativePathForDirectory(String directoryPath, String relativePath) { assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :") .that(extractRelativePathForDirectory(directoryPath)) .isEqualTo(relativePath); } private static ContentValues computeDataValues(String path) { final ContentValues values = new ContentValues(); values.put(MediaColumns.DATA, path); FileUtils.computeValuesFromData(values, /*forFuse*/ false); Log.v(TAG, "Computed values " + values); return values; } private static void assertBucket(ContentValues values, String bucketId, String bucketName) { if (bucketId != null) { assertEquals(bucketName, values.getAsString(ImageColumns.BUCKET_DISPLAY_NAME)); assertEquals(bucketId.toLowerCase(Locale.ROOT).hashCode(), (long) values.getAsLong(ImageColumns.BUCKET_ID)); } else { assertNull(values.get(ImageColumns.BUCKET_DISPLAY_NAME)); assertNull(values.get(ImageColumns.BUCKET_ID)); } } private static void assertVolume(ContentValues values, String volumeName) { assertEquals(volumeName, values.getAsString(ImageColumns.VOLUME_NAME)); } private static void assertRelativePath(ContentValues values, String relativePath) { assertEquals(relativePath, values.get(ImageColumns.RELATIVE_PATH)); } private static void assertMimetype(ContentValues values, String type) { assertEquals(type, values.get(MediaColumns.MIME_TYPE)); } private static void assertDisplayName(ContentValues values, String type) { assertEquals(type, values.get(MediaColumns.DISPLAY_NAME)); } private static boolean isGreylistMatch(String raw) { for (Pattern p : MediaProvider.sGreylist) { if (p.matcher(raw).matches()) { return true; } } return false; } private String buildFile(Uri uri, String relativePath, String displayName, String mimeType) { final ContentValues values = new ContentValues(); if (relativePath != null) { values.put(MediaColumns.RELATIVE_PATH, relativePath); } values.put(MediaColumns.DISPLAY_NAME, displayName); values.put(MediaColumns.MIME_TYPE, mimeType); try { ensureFileColumns(uri, values); } catch (VolumeArgumentException | VolumeNotFoundException e) { throw e.rethrowAsIllegalArgumentException(); } return values.getAsString(MediaColumns.DATA); } private void ensureFileColumns(Uri uri, ContentValues values) throws VolumeArgumentException, VolumeNotFoundException { try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { ((MediaProvider) cpc.getLocalContentProvider()) .ensureFileColumns(uri, values); } } private static void assertEndsWith(String expected, String actual) { if (!actual.endsWith(expected)) { fail("Expected ends with " + expected + " but found " + actual); } } private static void assertThrows(Class clazz, Runnable r) { try { r.run(); fail("Expected " + clazz + " to be thrown"); } catch (Exception e) { if (!clazz.isAssignableFrom(e.getClass())) { throw e; } } } @Test public void testNestedTransaction_applyBatch() throws Exception { final Uri[] uris = new Uri[]{ MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, 0), MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 0), }; final ArrayList ops = new ArrayList<>(); ops.add(ContentProviderOperation.newDelete(uris[0]).build()); ops.add(ContentProviderOperation.newDelete(uris[1]).build()); sIsolatedResolver.applyBatch(MediaStore.AUTHORITY, ops); } @Test public void testRedactionForInvalidUris() throws Exception { try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); final String volumeName = MediaStore.VOLUME_EXTERNAL; assertNull(mp.getRedactedUri(MediaStore.Images.Media.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Video.Media.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Audio.Media.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Audio.Albums.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Audio.Artists.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Audio.Genres.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Audio.Playlists.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Downloads.getContentUri(volumeName))); assertNull(mp.getRedactedUri(MediaStore.Files.getContentUri(volumeName))); // Check with a very large value - which shouldn't be present normally (at least for // tests). assertNull(mp.getRedactedUri( MediaStore.Images.Media.getContentUri(volumeName, Long.MAX_VALUE))); } } @Test public void testRedactionForInvalidAndValidUris() throws Exception { final String volumeName = MediaStore.VOLUME_EXTERNAL; final List uris = new ArrayList<>(); uris.add(MediaStore.Images.Media.getContentUri(volumeName)); uris.add(MediaStore.Video.Media.getContentUri(volumeName)); final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); final File[] files = new File[]{ stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")), stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")), stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg")) }; try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); for (File file : files) { uris.add(MediaStore.scanFile(sIsolatedResolver, file)); } List redactedUris = mp.getRedactedUri(uris); assertEquals(uris.size(), redactedUris.size()); assertNull(redactedUris.get(0)); assertNull(redactedUris.get(1)); assertNotNull(redactedUris.get(2)); assertNotNull(redactedUris.get(3)); assertNotNull(redactedUris.get(4)); } finally { for (File file : files) { file.delete(); } } } @Test public void testRedactionForFileExtension() throws Exception { testRedactionForFileExtension(R.raw.test_audio, ".mp3"); testRedactionForFileExtension(R.raw.test_video_xmp, ".mp4"); testRedactionForFileExtension(R.raw.lg_g4_iso_800_jpg, ".jpg"); } private void testRedactionForFileExtension(int resId, String extension) throws Exception { final File dir = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); final File file = new File(dir, "test" + System.nanoTime() + extension); stage(resId, file); final List uris = new ArrayList<>(); uris.add(MediaStore.scanFile(sIsolatedResolver, file)); try (ContentProviderClient cpc = sIsolatedResolver .acquireContentProviderClient(MediaStore.AUTHORITY)) { final MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); final String[] projection = new String[]{MediaColumns.DISPLAY_NAME, MediaColumns.DATA}; for (Uri uri : mp.getRedactedUri(uris)) { try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { assertNotNull(c); assertEquals(1, c.getCount()); assertTrue(c.moveToFirst()); assertTrue(c.getString(0).endsWith(extension)); assertTrue(c.getString(1).endsWith(extension)); } } } finally { file.delete(); } } }