• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 com.android.providers.media;
18 
19 import static com.android.providers.media.scan.MediaScannerTest.stage;
20 import static com.android.providers.media.util.FileUtils.extractDisplayName;
21 import static com.android.providers.media.util.FileUtils.extractRelativePath;
22 import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
23 import static com.android.providers.media.util.FileUtils.isDownload;
24 import static com.android.providers.media.util.FileUtils.isDownloadDir;
25 
26 import static com.google.common.truth.Truth.assertThat;
27 import static com.google.common.truth.Truth.assertWithMessage;
28 
29 import static org.junit.Assert.assertArrayEquals;
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.assertFalse;
32 import static org.junit.Assert.assertNotEquals;
33 import static org.junit.Assert.assertNotNull;
34 import static org.junit.Assert.assertNull;
35 import static org.junit.Assert.assertTrue;
36 import static org.junit.Assert.fail;
37 
38 import android.Manifest;
39 import android.content.ContentProviderClient;
40 import android.content.ContentProviderOperation;
41 import android.content.ContentResolver;
42 import android.content.ContentUris;
43 import android.content.ContentValues;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.PackageManager;
47 import android.content.pm.ProviderInfo;
48 import android.database.Cursor;
49 import android.net.Uri;
50 import android.os.Build;
51 import android.os.Bundle;
52 import android.os.CancellationSignal;
53 import android.os.Environment;
54 import android.os.UserHandle;
55 import android.provider.MediaStore;
56 import android.provider.MediaStore.Audio.AudioColumns;
57 import android.provider.MediaStore.Files.FileColumns;
58 import android.provider.MediaStore.Images.ImageColumns;
59 import android.provider.MediaStore.MediaColumns;
60 import android.util.ArrayMap;
61 import android.util.Log;
62 
63 import androidx.test.InstrumentationRegistry;
64 import androidx.test.runner.AndroidJUnit4;
65 
66 import com.android.providers.media.MediaProvider.FallbackException;
67 import com.android.providers.media.MediaProvider.VolumeArgumentException;
68 import com.android.providers.media.MediaProvider.VolumeNotFoundException;
69 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
70 import com.android.providers.media.util.FileUtils;
71 import com.android.providers.media.util.FileUtilsTest;
72 import com.android.providers.media.util.SQLiteQueryBuilder;
73 
74 import org.junit.AfterClass;
75 import org.junit.Assume;
76 import org.junit.BeforeClass;
77 import org.junit.Ignore;
78 import org.junit.Test;
79 import org.junit.runner.RunWith;
80 
81 import java.io.ByteArrayOutputStream;
82 import java.io.File;
83 import java.io.PrintWriter;
84 import java.sql.Array;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.Collection;
88 import java.util.Collections;
89 import java.util.List;
90 import java.util.Locale;
91 import java.util.regex.Pattern;
92 
93 @RunWith(AndroidJUnit4.class)
94 public class MediaProviderTest {
95     static final String TAG = "MediaProviderTest";
96 
97     // The test app without permissions
98     static final String PERMISSIONLESS_APP = "com.android.providers.media.testapp.withoutperms";
99 
100     private static Context sIsolatedContext;
101     private static ContentResolver sIsolatedResolver;
102 
103     @BeforeClass
setUp()104     public static void setUp() {
105         InstrumentationRegistry.getInstrumentation().getUiAutomation()
106                 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
107                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
108                         Manifest.permission.READ_DEVICE_CONFIG,
109                         Manifest.permission.INTERACT_ACROSS_USERS);
110 
111         final Context context = InstrumentationRegistry.getTargetContext();
112         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
113         sIsolatedResolver = sIsolatedContext.getContentResolver();
114     }
115 
116     @AfterClass
tearDown()117     public static void tearDown() {
118         InstrumentationRegistry.getInstrumentation()
119                 .getUiAutomation().dropShellPermissionIdentity();
120     }
121 
122     /**
123      * To fully exercise all our tests, we require that the Cuttlefish emulator
124      * have both emulated primary storage and an SD card be present.
125      */
126     @Test
testCuttlefish()127     public void testCuttlefish() {
128         Assume.assumeTrue(Build.MODEL.contains("Cuttlefish"));
129 
130         assertTrue("Cuttlefish must have both emulated storage and an SD card to exercise tests",
131                 MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext())
132                         .size() > 1);
133     }
134 
135     @Test
testSchema()136     public void testSchema() {
137         for (String path : new String[] {
138                 "images/media",
139                 "images/media/1",
140                 "images/thumbnails",
141                 "images/thumbnails/1",
142 
143                 "audio/media",
144                 "audio/media/1",
145                 "audio/media/1/genres",
146                 "audio/media/1/genres/1",
147                 "audio/genres",
148                 "audio/genres/1",
149                 "audio/genres/1/members",
150                 "audio/playlists",
151                 "audio/playlists/1",
152                 "audio/playlists/1/members",
153                 "audio/playlists/1/members/1",
154                 "audio/artists",
155                 "audio/artists/1",
156                 "audio/artists/1/albums",
157                 "audio/albums",
158                 "audio/albums/1",
159                 "audio/albumart",
160                 "audio/albumart/1",
161 
162                 "video/media",
163                 "video/media/1",
164                 "video/thumbnails",
165                 "video/thumbnails/1",
166 
167                 "file",
168                 "file/1",
169 
170                 "downloads",
171                 "downloads/1",
172         }) {
173             final Uri probe = MediaStore.AUTHORITY_URI.buildUpon()
174                     .appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build();
175             try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) {
176                 assertNotNull("probe", c);
177             }
178             try {
179                 sIsolatedResolver.getType(probe);
180             } catch (IllegalStateException tolerated) {
181             }
182         }
183     }
184 
185     @Test
testLocale()186     public void testLocale() {
187         try (ContentProviderClient cpc = sIsolatedResolver
188                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
189             ((MediaProvider) cpc.getLocalContentProvider())
190                     .onLocaleChanged();
191         }
192     }
193 
194     @Test
testDump()195     public void testDump() throws Exception {
196         try (ContentProviderClient cpc = sIsolatedResolver
197                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
198             cpc.getLocalContentProvider().dump(null,
199                     new PrintWriter(new ByteArrayOutputStream()), null);
200         }
201     }
202 
203     /**
204      * Verify that our fallback exceptions throw on modern apps while degrading
205      * gracefully for legacy apps.
206      */
207     @Test
testFallbackException()208     public void testFallbackException() throws Exception {
209         for (FallbackException e : new FallbackException[] {
210                 new FallbackException("test", Build.VERSION_CODES.Q),
211                 new VolumeNotFoundException("test"),
212                 new VolumeArgumentException(new File("/"), Collections.emptyList())
213         }) {
214             // Modern apps should get thrown
215             assertThrows(Exception.class, () -> {
216                 e.translateForInsert(Build.VERSION_CODES.CUR_DEVELOPMENT);
217             });
218             assertThrows(Exception.class, () -> {
219                 e.translateForUpdateDelete(Build.VERSION_CODES.CUR_DEVELOPMENT);
220             });
221             assertThrows(Exception.class, () -> {
222                 e.translateForQuery(Build.VERSION_CODES.CUR_DEVELOPMENT);
223             });
224 
225             // Legacy apps gracefully log without throwing
226             assertEquals(null, e.translateForInsert(Build.VERSION_CODES.BASE));
227             assertEquals(0, e.translateForUpdateDelete(Build.VERSION_CODES.BASE));
228             assertEquals(null, e.translateForQuery(Build.VERSION_CODES.BASE));
229         }
230     }
231 
232     /**
233      * We already have solid coverage of this logic in {@link IdleServiceTest},
234      * but the coverage system currently doesn't measure that, so we add the
235      * bare minimum local testing here to convince the tooling that it's
236      * covered.
237      */
238     @Test
testIdle()239     public void testIdle() throws Exception {
240         try (ContentProviderClient cpc = sIsolatedResolver
241                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
242             ((MediaProvider) cpc.getLocalContentProvider())
243                     .onIdleMaintenance(new CancellationSignal());
244         }
245     }
246 
247     /**
248      * We already have solid coverage of this logic in
249      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
250      * measure that, so we add the bare minimum local testing here to convince
251      * the tooling that it's covered.
252      */
253     @Test
testCanonicalize()254     public void testCanonicalize() throws Exception {
255         // We might have old files lurking, so force a clean slate
256         final Context context = InstrumentationRegistry.getTargetContext();
257         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
258         sIsolatedResolver = sIsolatedContext.getContentResolver();
259 
260         final File dir = Environment
261                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
262         for (File file : new File[] {
263                 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")),
264                 stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")),
265                 stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg"))
266         }) {
267             final Uri uri = MediaStore.scanFile(sIsolatedResolver, file);
268             Log.v(TAG, "Scanned " + file + " as " + uri);
269 
270             final Uri forward = sIsolatedResolver.canonicalize(uri);
271             final Uri reverse = sIsolatedResolver.uncanonicalize(forward);
272 
273             assertEquals(ContentUris.parseId(uri), ContentUris.parseId(forward));
274             assertEquals(ContentUris.parseId(uri), ContentUris.parseId(reverse));
275         }
276     }
277 
278     /**
279      * We already have solid coverage of this logic in
280      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
281      * measure that, so we add the bare minimum local testing here to convince
282      * the tooling that it's covered.
283      */
284     @Test
testMetadata()285     public void testMetadata() {
286         assertNotNull(MediaStore.getVersion(sIsolatedContext,
287                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
288         assertNotNull(MediaStore.getGeneration(sIsolatedResolver,
289                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
290     }
291 
292     /**
293      * We already have solid coverage of this logic in
294      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
295      * measure that, so we add the bare minimum local testing here to convince
296      * the tooling that it's covered.
297      */
298     @Test
testCreateRequest()299     public void testCreateRequest() throws Exception {
300         final Collection<Uri> uris = Arrays.asList(
301                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42));
302         assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris));
303     }
304 
305     /**
306      * We already have solid coverage of this logic in
307      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
308      * measure that, so we add the bare minimum local testing here to convince
309      * the tooling that it's covered.
310      */
311     @Test
testCheckUriPermission()312     public void testCheckUriPermission() throws Exception {
313         final ContentValues values = new ContentValues();
314         values.put(MediaColumns.DISPLAY_NAME, "test.mp3");
315         values.put(MediaColumns.MIME_TYPE, "audio/mpeg");
316         final Uri uri = sIsolatedResolver.insert(
317                 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
318 
319         assertEquals(PackageManager.PERMISSION_GRANTED, sIsolatedResolver.checkUriPermission(uri,
320                 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
321     }
322 
323     @Test
testTrashLongFileNameItemHasTrimmedFileName()324     public void testTrashLongFileNameItemHasTrimmedFileName() throws Exception {
325         testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_TRASHED);
326     }
327 
328     @Test
testPendingLongFileNameItemHasTrimmedFileName()329     public void testPendingLongFileNameItemHasTrimmedFileName() throws Exception {
330         testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_PENDING);
331     }
332 
testActionLongFileNameItemHasTrimmedFileName(String columnKey)333     private void testActionLongFileNameItemHasTrimmedFileName(String columnKey) throws Exception {
334         // We might have old files lurking, so force a clean slate
335         final Context context = InstrumentationRegistry.getTargetContext();
336         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
337         sIsolatedResolver = sIsolatedContext.getContentResolver();
338         final String[] projection = new String[]{MediaColumns.DATA};
339         final File dir = Environment
340                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
341 
342         // create extreme long file name
343         final String originalName = FileUtilsTest.createExtremeFileName("test" + System.nanoTime(),
344                 ".jpg");
345 
346         File file = stage(R.raw.lg_g4_iso_800_jpg, new File(dir, originalName));
347         final Uri uri = MediaStore.scanFile(sIsolatedResolver, file);
348         Log.v(TAG, "Scanned " + file + " as " + uri);
349 
350         try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) {
351             assertNotNull(c);
352             assertEquals(1, c.getCount());
353             assertTrue(c.moveToFirst());
354             final String data = c.getString(0);
355             final String result = FileUtils.extractDisplayName(data);
356             assertEquals(originalName, result);
357         }
358 
359         final Bundle extras = new Bundle();
360         extras.putBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true);
361         final ContentValues values = new ContentValues();
362         values.put(columnKey, 1);
363         sIsolatedResolver.update(uri, values, extras);
364 
365         try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) {
366             assertNotNull(c);
367             assertEquals(1, c.getCount());
368             assertTrue(c.moveToFirst());
369             final String data = c.getString(0);
370             final String result = FileUtils.extractDisplayName(data);
371             assertThat(result.length()).isAtMost(FileUtilsTest.MAX_FILENAME_BYTES);
372             assertNotEquals(originalName, result);
373         }
374     }
375 
376     /**
377      * We already have solid coverage of this logic in
378      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
379      * measure that, so we add the bare minimum local testing here to convince
380      * the tooling that it's covered.
381      */
382     @Test
testBulkInsert()383     public void testBulkInsert() throws Exception {
384         final ContentValues values1 = new ContentValues();
385         values1.put(MediaColumns.DISPLAY_NAME, "test1.mp3");
386         values1.put(MediaColumns.MIME_TYPE, "audio/mpeg");
387 
388         final ContentValues values2 = new ContentValues();
389         values2.put(MediaColumns.DISPLAY_NAME, "test2.mp3");
390         values2.put(MediaColumns.MIME_TYPE, "audio/mpeg");
391 
392         final Uri targetUri = MediaStore.Audio.Media
393                 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
394         assertEquals(2, sIsolatedResolver.bulkInsert(targetUri,
395                 new ContentValues[] { values1, values2 }));
396     }
397 
398     /**
399      * We already have solid coverage of this logic in
400      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
401      * measure that, so we add the bare minimum local testing here to convince
402      * the tooling that it's covered.
403      */
404     @Test
testCustomCollator()405     public void testCustomCollator() throws Exception {
406         final Bundle extras = new Bundle();
407         extras.putString(ContentResolver.QUERY_ARG_SORT_LOCALE, "en");
408 
409         try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI,
410                 null, extras, null)) {
411             assertNotNull(c);
412         }
413     }
414 
415     /**
416      * We already have solid coverage of this logic in
417      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
418      * measure that, so we add the bare minimum local testing here to convince
419      * the tooling that it's covered.
420      */
421     @Test
testGetRedactionRanges_Image()422     public void testGetRedactionRanges_Image() throws Exception {
423         final File file = File.createTempFile("test", ".jpg");
424         stage(R.raw.test_image, file);
425         assertNotNull(MediaProvider.getRedactionRanges(file));
426     }
427 
428     /**
429      * We already have solid coverage of this logic in
430      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
431      * measure that, so we add the bare minimum local testing here to convince
432      * the tooling that it's covered.
433      */
434     @Test
testGetRedactionRanges_Video()435     public void testGetRedactionRanges_Video() throws Exception {
436         final File file = File.createTempFile("test", ".mp4");
437         stage(R.raw.test_video, file);
438         assertNotNull(MediaProvider.getRedactionRanges(file));
439     }
440 
441     @Test
testComputeCommonPrefix_Single()442     public void testComputeCommonPrefix_Single() {
443         assertEquals(Uri.parse("content://authority/1/2/3"),
444                 MediaProvider.computeCommonPrefix(Arrays.asList(
445                         Uri.parse("content://authority/1/2/3"))));
446     }
447 
448     @Test
testComputeCommonPrefix_Deeper()449     public void testComputeCommonPrefix_Deeper() {
450         assertEquals(Uri.parse("content://authority/1/2/3"),
451                 MediaProvider.computeCommonPrefix(Arrays.asList(
452                         Uri.parse("content://authority/1/2/3/4"),
453                         Uri.parse("content://authority/1/2/3/4/5"),
454                         Uri.parse("content://authority/1/2/3"))));
455     }
456 
457     @Test
testComputeCommonPrefix_Siblings()458     public void testComputeCommonPrefix_Siblings() {
459         assertEquals(Uri.parse("content://authority/1/2"),
460                 MediaProvider.computeCommonPrefix(Arrays.asList(
461                         Uri.parse("content://authority/1/2/3"),
462                         Uri.parse("content://authority/1/2/99"))));
463     }
464 
465     @Test
testComputeCommonPrefix_Drastic()466     public void testComputeCommonPrefix_Drastic() {
467         assertEquals(Uri.parse("content://authority"),
468                 MediaProvider.computeCommonPrefix(Arrays.asList(
469                         Uri.parse("content://authority/1/2/3"),
470                         Uri.parse("content://authority/99/99/99"))));
471     }
472 
getPathOwnerPackageName(String path)473     private static String getPathOwnerPackageName(String path) {
474         return FileUtils.extractPathOwnerPackageName(path);
475     }
476 
477     @Test
testPathOwnerPackageName_None()478     public void testPathOwnerPackageName_None() throws Exception {
479         assertEquals(null, getPathOwnerPackageName(null));
480         assertEquals(null, getPathOwnerPackageName("/data/path"));
481     }
482 
483     @Test
testPathOwnerPackageName_Emulated()484     public void testPathOwnerPackageName_Emulated() throws Exception {
485         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/DCIM/foo.jpg"));
486         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/"));
487         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/data/"));
488 
489         assertEquals("com.example",
490                 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/"));
491         assertEquals("com.example",
492                 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/foo.jpg"));
493         assertEquals("com.example",
494                 getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg"));
495         assertEquals("com.example",
496                 getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg"));
497     }
498 
499     @Test
testPathOwnerPackageName_Portable()500     public void testPathOwnerPackageName_Portable() throws Exception {
501         assertEquals(null, getPathOwnerPackageName("/storage/0000-0000/DCIM/foo.jpg"));
502 
503         assertEquals("com.example",
504                 getPathOwnerPackageName("/storage/0000-0000/Android/data/com.example/foo.jpg"));
505     }
506 
507     @Test
testBuildData_Simple()508     public void testBuildData_Simple() throws Exception {
509         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
510         assertEndsWith("/Pictures/file.png",
511                 buildFile(uri, null, "file", "image/png"));
512         assertEndsWith("/Pictures/file.png",
513                 buildFile(uri, null, "file.png", "image/png"));
514         assertEndsWith("/Pictures/file.jpg.png",
515                 buildFile(uri, null, "file.jpg", "image/png"));
516     }
517 
518     @Test
testBuildData_withUserId()519     public void testBuildData_withUserId() throws Exception {
520         final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
521         final ContentValues values = new ContentValues();
522         values.put(MediaColumns.DISPLAY_NAME, "test_userid");
523         values.put(MediaColumns.MIME_TYPE, "image/png");
524         Uri result = sIsolatedResolver.insert(uri, values);
525         try (Cursor c = sIsolatedResolver.query(result,
526                 new String[]{MediaColumns.DISPLAY_NAME, FileColumns._USER_ID},
527                 null, null)) {
528             assertNotNull(c);
529             assertEquals(1, c.getCount());
530             assertTrue(c.moveToFirst());
531             assertEquals("test_userid.png", c.getString(0));
532             assertEquals(UserHandle.myUserId(), c.getInt(1));
533         }
534     }
535 
536     @Test
testBuildData_Primary()537     public void testBuildData_Primary() throws Exception {
538         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
539         assertEndsWith("/DCIM/IMG_1024.JPG",
540                 buildFile(uri, Environment.DIRECTORY_DCIM, "IMG_1024.JPG", "image/jpeg"));
541     }
542 
543     @Test
544     @Ignore("Enable as part of b/142561358")
testBuildData_Secondary()545     public void testBuildData_Secondary() throws Exception {
546         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
547         assertEndsWith("/Pictures/Screenshots/foo.png",
548                 buildFile(uri, "Pictures/Screenshots", "foo.png", "image/png"));
549     }
550 
551     @Test
testBuildData_InvalidNames()552     public void testBuildData_InvalidNames() throws Exception {
553         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
554         assertEndsWith("/Pictures/foo_bar.png",
555             buildFile(uri, null, "foo/bar", "image/png"));
556         assertEndsWith("/Pictures/_.hidden.png",
557             buildFile(uri, null, ".hidden", "image/png"));
558     }
559 
560     @Test
testBuildData_InvalidTypes()561     public void testBuildData_InvalidTypes() throws Exception {
562         for (String type : new String[] {
563                 "audio/foo", "video/foo", "image/foo", "application/foo", "foo/foo"
564         }) {
565             if (!type.startsWith("audio/")) {
566                 assertThrows(IllegalArgumentException.class, () -> {
567                     buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
568                             null, "foo", type);
569                 });
570             }
571             if (!type.startsWith("video/")) {
572                 assertThrows(IllegalArgumentException.class, () -> {
573                     buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
574                             null, "foo", type);
575                 });
576             }
577             if (!type.startsWith("image/")) {
578                 assertThrows(IllegalArgumentException.class, () -> {
579                     buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
580                             null, "foo", type);
581                 });
582             }
583         }
584     }
585 
586     @Test
testBuildData_InvalidSecondaryTypes()587     public void testBuildData_InvalidSecondaryTypes() throws Exception {
588         assertEndsWith("/Pictures/foo.png",
589                 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
590                         null, "foo.png", "image/*"));
591 
592         assertThrows(IllegalArgumentException.class, () -> {
593             buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
594                     null, "foo", "video/*");
595         });
596         assertThrows(IllegalArgumentException.class, () -> {
597             buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
598                     null, "foo.mp4", "audio/*");
599         });
600     }
601 
602     @Test
testBuildData_EmptyTypes()603     public void testBuildData_EmptyTypes() throws Exception {
604         Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
605         assertEndsWith("/Pictures/foo.png",
606                 buildFile(uri, null, "foo.png", ""));
607 
608         uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
609         assertEndsWith(".mp4",
610                 buildFile(uri, null, "", ""));
611     }
612 
613     @Test
testEnsureFileColumns_InvalidMimeType_targetSdkQ()614     public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception {
615         final MediaProvider provider = new MediaProvider() {
616             @Override
617             public boolean isFuseThread() {
618                 return false;
619             }
620 
621             @Override
622             public int getCallingPackageTargetSdkVersion() {
623                 return Build.VERSION_CODES.Q;
624             }
625         };
626 
627         final ProviderInfo info = sIsolatedContext.getPackageManager()
628                 .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
629         // Attach providerInfo, to make sure mCallingIdentity can be populated
630         provider.attachInfo(sIsolatedContext, info);
631         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
632         final ContentValues values = new ContentValues();
633 
634         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
635         provider.ensureFileColumns(uri, values);
636         assertMimetype(values, "image/jpeg");
637         assertDisplayName(values, "pngimage.png.jpg");
638 
639         values.clear();
640         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
641         values.put(MediaColumns.MIME_TYPE, "");
642         provider.ensureFileColumns(uri, values);
643         assertMimetype(values, "image/jpeg");
644         assertDisplayName(values, "pngimage.png.jpg");
645 
646         values.clear();
647         values.put(MediaColumns.MIME_TYPE, "");
648         provider.ensureFileColumns(uri, values);
649         assertMimetype(values, "image/jpeg");
650 
651         values.clear();
652         values.put(MediaColumns.DISPLAY_NAME, "foo.foo");
653         provider.ensureFileColumns(uri, values);
654         assertMimetype(values, "image/jpeg");
655         assertDisplayName(values, "foo.foo.jpg");
656     }
657 
658     @Ignore("Enable as part of b/142561358")
testBuildData_Charset()659     public void testBuildData_Charset() throws Exception {
660         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
661         assertEndsWith("/Pictures/foo__bar/bar__baz.png",
662                 buildFile(uri, "Pictures/foo\0\0bar", "bar::baz.png", "image/png"));
663     }
664 
665     @Test
testBuildData_Playlists()666     public void testBuildData_Playlists() throws Exception {
667         final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
668         assertEndsWith("/Music/my_playlist.m3u",
669                 buildFile(uri, null, "my_playlist", "audio/mpegurl"));
670         assertEndsWith("/Movies/my_playlist.pls",
671                 buildFile(uri, "Movies", "my_playlist", "audio/x-scpls"));
672     }
673 
674     @Test
testBuildData_Subtitles()675     public void testBuildData_Subtitles() throws Exception {
676         final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
677         assertEndsWith("/Movies/my_subtitle.srt",
678                 buildFile(uri, null, "my_subtitle", "application/x-subrip"));
679         assertEndsWith("/Music/my_lyrics.lrc",
680                 buildFile(uri, "Music", "my_lyrics", "application/lrc"));
681     }
682 
683     @Test
testBuildData_Downloads()684     public void testBuildData_Downloads() throws Exception {
685         final Uri uri = MediaStore.Downloads
686                 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
687         assertEndsWith("/Download/linux.iso",
688                 buildFile(uri, null, "linux.iso", "application/x-iso9660-image"));
689     }
690 
691     @Test
testBuildData_Pending_FromValues()692     public void testBuildData_Pending_FromValues() throws Exception {
693         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
694         final ContentValues forward = new ContentValues();
695         forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
696         forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
697         forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
698         forward.put(MediaColumns.IS_PENDING, 1);
699         forward.put(MediaColumns.IS_TRASHED, 0);
700         forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
701         ensureFileColumns(uri, forward);
702 
703         // Requested filename remains intact, but raw path on disk is mutated to
704         // reflect that it's a pending item with a specific expiration time
705         assertEquals("IMG1024.JPG",
706                 forward.getAsString(MediaColumns.DISPLAY_NAME));
707         assertEndsWith(".pending-1577836800-IMG1024.JPG",
708                 forward.getAsString(MediaColumns.DATA));
709     }
710 
711     @Test
testBuildData_Pending_FromValues_differentLocale()712     public void testBuildData_Pending_FromValues_differentLocale() throws Exception {
713         // See b/174120008 for context.
714         Locale defaultLocale = Locale.getDefault();
715         try {
716             Locale.setDefault(new Locale("ar", "SA"));
717             testBuildData_Pending_FromValues();
718         } finally {
719             Locale.setDefault(defaultLocale);
720         }
721     }
722 
723     @Test
testBuildData_Pending_FromData()724     public void testBuildData_Pending_FromData() throws Exception {
725         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
726         final ContentValues reverse = new ContentValues();
727         reverse.put(MediaColumns.DATA,
728                 "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG");
729         ensureFileColumns(uri, reverse);
730 
731         assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
732         assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
733         assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
734         assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
735         assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
736         assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
737     }
738 
739     @Test
testBuildData_Trashed_FromValues()740     public void testBuildData_Trashed_FromValues() throws Exception {
741         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
742         final ContentValues forward = new ContentValues();
743         forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
744         forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
745         forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
746         forward.put(MediaColumns.IS_PENDING, 0);
747         forward.put(MediaColumns.IS_TRASHED, 1);
748         forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
749         ensureFileColumns(uri, forward);
750 
751         // Requested filename remains intact, but raw path on disk is mutated to
752         // reflect that it's a trashed item with a specific expiration time
753         assertEquals("IMG1024.JPG",
754                 forward.getAsString(MediaColumns.DISPLAY_NAME));
755         assertEndsWith(".trashed-1577836800-IMG1024.JPG",
756                 forward.getAsString(MediaColumns.DATA));
757     }
758 
759     @Test
testBuildData_Trashed_FromValues_differentLocale()760     public void testBuildData_Trashed_FromValues_differentLocale() throws Exception {
761         // See b/174120008 for context.
762         Locale defaultLocale = Locale.getDefault();
763         try {
764             Locale.setDefault(new Locale("ar", "SA"));
765             testBuildData_Trashed_FromValues();
766         } finally {
767             Locale.setDefault(defaultLocale);
768         }
769     }
770 
771     @Test
testBuildData_Trashed_FromData()772     public void testBuildData_Trashed_FromData() throws Exception {
773         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
774         final ContentValues reverse = new ContentValues();
775         reverse.put(MediaColumns.DATA,
776                 "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG");
777         ensureFileColumns(uri, reverse);
778 
779         assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
780         assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
781         assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
782         assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
783         assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
784         assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
785     }
786 
787     @Test
testGreylist()788     public void testGreylist() throws Exception {
789         assertFalse(isGreylistMatch(
790                 "SELECT secret FROM other_table"));
791 
792         assertTrue(isGreylistMatch(
793                 "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"));
794         assertTrue(isGreylistMatch(
795                 "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)"));
796         assertTrue(isGreylistMatch(
797                 "0 as orientation"));
798         assertTrue(isGreylistMatch(
799                 "\"content://media/internal/audio/media\""));
800     }
801 
802     @Test
testGreylist_115845887()803     public void testGreylist_115845887() {
804         assertTrue(isGreylistMatch(
805                 "MAX(*)"));
806         assertTrue(isGreylistMatch(
807                 "MAX(_id)"));
808 
809         assertTrue(isGreylistMatch(
810                 "sum(column_name)"));
811         assertFalse(isGreylistMatch(
812                 "SUM(foo+bar)"));
813 
814         assertTrue(isGreylistMatch(
815                 "count(column_name)"));
816         assertFalse(isGreylistMatch(
817                 "count(other_table.column_name)"));
818     }
819 
820     @Test
testGreylist_116489751_116135586_116117120_116084561_116074030_116062802()821     public void testGreylist_116489751_116135586_116117120_116084561_116074030_116062802() {
822         assertTrue(isGreylistMatch(
823                 "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)"));
824     }
825 
826     @Test
testGreylist_116699470()827     public void testGreylist_116699470() {
828         assertTrue(isGreylistMatch(
829                 "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)"));
830     }
831 
832     @Test
testGreylist_116531759()833     public void testGreylist_116531759() {
834         assertTrue(isGreylistMatch(
835                 "count(*)"));
836         assertTrue(isGreylistMatch(
837                 "COUNT(*)"));
838         assertFalse(isGreylistMatch(
839                 "xCOUNT(*)"));
840         assertTrue(isGreylistMatch(
841                 "count(*) AS image_count"));
842         assertTrue(isGreylistMatch(
843                 "count(_id)"));
844         assertTrue(isGreylistMatch(
845                 "count(_id) AS image_count"));
846 
847         assertTrue(isGreylistMatch(
848                 "column_a AS column_b"));
849         assertFalse(isGreylistMatch(
850                 "other_table.column_a AS column_b"));
851     }
852 
853     @Test
testGreylist_118475754()854     public void testGreylist_118475754() {
855         assertTrue(isGreylistMatch(
856                 "count(*) pcount"));
857         assertTrue(isGreylistMatch(
858                 "foo AS bar"));
859         assertTrue(isGreylistMatch(
860                 "foo bar"));
861         assertTrue(isGreylistMatch(
862                 "count(foo) AS bar"));
863         assertTrue(isGreylistMatch(
864                 "count(foo) bar"));
865     }
866 
867     @Test
testGreylist_119522660()868     public void testGreylist_119522660() {
869         assertTrue(isGreylistMatch(
870                 "CAST(_id AS TEXT) AS string_id"));
871         assertTrue(isGreylistMatch(
872                 "cast(_id as text)"));
873     }
874 
875     @Test
testGreylist_126945991()876     public void testGreylist_126945991() {
877         assertTrue(isGreylistMatch(
878                 "substr(_data, length(_data)-length(_display_name), 1) as filename_prevchar"));
879     }
880 
881     @Test
testGreylist_127900881()882     public void testGreylist_127900881() {
883         assertTrue(isGreylistMatch(
884                 "*"));
885     }
886 
887     @Test
testGreylist_128389972()888     public void testGreylist_128389972() {
889         assertTrue(isGreylistMatch(
890                 " count(bucket_id) images_count"));
891     }
892 
893     @Test
testGreylist_129746861()894     public void testGreylist_129746861() {
895         assertTrue(isGreylistMatch(
896                 "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"));
897     }
898 
899     @Test
testGreylist_114112523()900     public void testGreylist_114112523() {
901         assertTrue(isGreylistMatch(
902                 "audio._id AS _id"));
903     }
904 
905     @Test
testComputeProjection_AggregationAllowed()906     public void testComputeProjection_AggregationAllowed() throws Exception {
907         final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
908         final ArrayMap<String, String> map = new ArrayMap<>();
909         map.put("external", "internal");
910         builder.setProjectionMap(map);
911         builder.setStrict(true);
912         builder.setStrictColumns(true);
913 
914         assertArrayEquals(
915                 new String[] { "internal" },
916                 builder.computeProjection(null));
917         assertArrayEquals(
918                 new String[] { "internal" },
919                 builder.computeProjection(new String[] { "external" }));
920         assertThrows(IllegalArgumentException.class, () -> {
921             builder.computeProjection(new String[] { "internal" });
922         });
923         assertThrows(IllegalArgumentException.class, () -> {
924             builder.computeProjection(new String[] { "MIN(internal)" });
925         });
926         assertArrayEquals(
927                 new String[] { "MIN(internal)" },
928                 builder.computeProjection(new String[] { "MIN(external)" }));
929         assertThrows(IllegalArgumentException.class, () -> {
930             builder.computeProjection(new String[] { "FOO(external)" });
931         });
932     }
933 
934     @Test
testIsDownload()935     public void testIsDownload() throws Exception {
936         assertTrue(isDownload("/storage/emulated/0/Download/colors.png"));
937         assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
938         assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4"));
939         assertTrue(isDownload("/storage/0000-0000/Download/foo.txt"));
940 
941         assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png"));
942         assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png"));
943         assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt"));
944         assertFalse(isDownload("/storage/emulated/0/Download"));
945     }
946 
947     @Test
testIsDownloadDir()948     public void testIsDownloadDir() throws Exception {
949         assertTrue(isDownloadDir("/storage/emulated/0/Download"));
950 
951         assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png"));
952         assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/"));
953     }
954 
955     @Test
testComputeDataValues_Grouped()956     public void testComputeDataValues_Grouped() throws Exception {
957         for (String data : new String[] {
958                 "/storage/0000-0000/DCIM/Camera/IMG1024.JPG",
959                 "/storage/0000-0000/DCIM/Camera/iMg1024.JpG",
960                 "/storage/0000-0000/DCIM/Camera/IMG1024.CR2",
961                 "/storage/0000-0000/DCIM/Camera/IMG1024.BURST001.JPG",
962         }) {
963             final ContentValues values = computeDataValues(data);
964             assertVolume(values, "0000-0000");
965             assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
966             assertRelativePath(values, "DCIM/Camera/");
967         }
968     }
969 
970     @Test
testComputeDataValues_Extensions()971     public void testComputeDataValues_Extensions() throws Exception {
972         ContentValues values;
973 
974         values = computeDataValues("/storage/0000-0000/DCIM/Camera/IMG1024");
975         assertVolume(values, "0000-0000");
976         assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
977         assertRelativePath(values, "DCIM/Camera/");
978 
979         values = computeDataValues("/storage/0000-0000/DCIM/Camera/.foo");
980         assertVolume(values, "0000-0000");
981         assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
982         assertRelativePath(values, "DCIM/Camera/");
983 
984         values = computeDataValues("/storage/476A-17F8/123456/test.png");
985         assertVolume(values, "476a-17f8");
986         assertBucket(values, "/storage/476A-17F8/123456", "123456");
987         assertRelativePath(values, "123456/");
988 
989         values = computeDataValues("/storage/476A-17F8/123456/789/test.mp3");
990         assertVolume(values, "476a-17f8");
991         assertBucket(values, "/storage/476A-17F8/123456/789", "789");
992         assertRelativePath(values, "123456/789/");
993     }
994 
995     @Test
testComputeDataValues_DirectoriesInvalid()996     public void testComputeDataValues_DirectoriesInvalid() throws Exception {
997         for (String data : new String[] {
998                 "/storage/IMG1024.JPG",
999                 "/data/media/IMG1024.JPG",
1000                 "IMG1024.JPG",
1001         }) {
1002             final ContentValues values = computeDataValues(data);
1003             assertRelativePath(values, null);
1004         }
1005     }
1006 
1007     @Test
testComputeDataValues_Directories()1008     public void testComputeDataValues_Directories() throws Exception {
1009         ContentValues values;
1010 
1011         for (String top : new String[] {
1012                 "/storage/emulated/0",
1013         }) {
1014             values = computeDataValues(top + "/IMG1024.JPG");
1015             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1016             assertBucket(values, top, null);
1017             assertRelativePath(values, "/");
1018 
1019             values = computeDataValues(top + "/One/IMG1024.JPG");
1020             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1021             assertBucket(values, top + "/One", "One");
1022             assertRelativePath(values, "One/");
1023 
1024             values = computeDataValues(top + "/One/Two/IMG1024.JPG");
1025             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1026             assertBucket(values, top + "/One/Two", "Two");
1027             assertRelativePath(values, "One/Two/");
1028 
1029             values = computeDataValues(top + "/One/Two/Three/IMG1024.JPG");
1030             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1031             assertBucket(values, top + "/One/Two/Three", "Three");
1032             assertRelativePath(values, "One/Two/Three/");
1033         }
1034     }
1035 
1036     @Test
testEnsureFileColumns_resolvesMimeType()1037     public void testEnsureFileColumns_resolvesMimeType() throws Exception {
1038         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
1039         final ContentValues values = new ContentValues();
1040         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
1041 
1042         final MediaProvider provider = new MediaProvider() {
1043             @Override
1044             public boolean isFuseThread() {
1045                 return false;
1046             }
1047 
1048             @Override
1049             public int getCallingPackageTargetSdkVersion() {
1050                 return Build.VERSION_CODES.CUR_DEVELOPMENT;
1051             }
1052         };
1053         final ProviderInfo info = sIsolatedContext.getPackageManager()
1054                 .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
1055         // Attach providerInfo, to make sure mCallingIdentity can be populated
1056         provider.attachInfo(sIsolatedContext, info);
1057         provider.ensureFileColumns(uri, values);
1058 
1059         assertMimetype(values, "image/png");
1060     }
1061 
1062     @Test
testRelativePathForInvalidDirectories()1063     public void testRelativePathForInvalidDirectories() throws Exception {
1064         for (String path : new String[] {
1065                 "/storage/emulated",
1066                 "/storage",
1067                 "/data/media/Foo.jpg",
1068                 "Foo.jpg",
1069                 "storage/Foo"
1070         }) {
1071             assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
1072         }
1073     }
1074 
1075     @Test
testRelativePathForValidDirectories()1076     public void testRelativePathForValidDirectories() throws Exception {
1077         for (String prefix : new String[] {
1078                 "/storage/emulated/0",
1079                 "/storage/emulated/10",
1080                 "/storage/ABCD-1234"
1081         }) {
1082             assertRelativePathForDirectory(prefix, "/");
1083             assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/");
1084             assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/");
1085             assertRelativePathForDirectory(prefix + "/Z", "Z/");
1086             assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo",
1087                     "Android/media/com.example/Foo/");
1088         }
1089     }
1090 
1091     @Test
testComputeAudioKeyValues_167339595_differentAlbumIds()1092     public void testComputeAudioKeyValues_167339595_differentAlbumIds() throws Exception {
1093         // same album name, different album artists
1094         final ContentValues valuesOne = new ContentValues();
1095         valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1096         valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1097         valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3");
1098         valuesOne.put(AudioColumns.TITLE, "Clocks");
1099         valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood");
1100         valuesOne.put(AudioColumns.ALBUM_ARTIST, "Coldplay");
1101         valuesOne.put(AudioColumns.GENRE, "Rock");
1102         valuesOne.put(AudioColumns.IS_MUSIC, true);
1103 
1104         final ContentValues valuesTwo = new ContentValues();
1105         valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1106         valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1107         valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3");
1108         valuesTwo.put(AudioColumns.TITLE, "Sounds");
1109         valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood");
1110         valuesTwo.put(AudioColumns.ALBUM_ARTIST, "ColdplayTwo");
1111         valuesTwo.put(AudioColumns.GENRE, "Alternative rock");
1112         valuesTwo.put(AudioColumns.IS_MUSIC, true);
1113 
1114         MediaProvider.computeAudioKeyValues(valuesOne);
1115         final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID);
1116         MediaProvider.computeAudioKeyValues(valuesTwo);
1117         final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID);
1118 
1119         assertNotEquals(albumIdOne, albumIdTwo);
1120 
1121         // same album name, different paths, no album artists
1122         final ContentValues valuesThree = new ContentValues();
1123         valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1124         valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1125         valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3");
1126         valuesThree.put(AudioColumns.TITLE, "Silent");
1127         valuesThree.put(AudioColumns.ALBUM, "Rainbow");
1128         valuesThree.put(AudioColumns.ARTIST, "Sample1");
1129         valuesThree.put(AudioColumns.GENRE, "Rock");
1130         valuesThree.put(AudioColumns.IS_MUSIC, true);
1131 
1132         final ContentValues valuesFour = new ContentValues();
1133         valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1134         valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1135         valuesFour.put(FileColumns.DATA, "/storage/emulated/0/123456/Rainbow.mp3");
1136         valuesFour.put(AudioColumns.TITLE, "Rainbow");
1137         valuesFour.put(AudioColumns.ALBUM, "Rainbow");
1138         valuesFour.put(AudioColumns.ARTIST, "Sample2");
1139         valuesFour.put(AudioColumns.GENRE, "Alternative rock");
1140         valuesFour.put(AudioColumns.IS_MUSIC, true);
1141 
1142         MediaProvider.computeAudioKeyValues(valuesThree);
1143         final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID);
1144         MediaProvider.computeAudioKeyValues(valuesFour);
1145         final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID);
1146 
1147         assertNotEquals(albumIdThree, albumIdFour);
1148     }
1149 
1150     @Test
testComputeAudioKeyValues_167339595_sameAlbumId()1151     public void testComputeAudioKeyValues_167339595_sameAlbumId() throws Exception {
1152         // same album name, same path, no album artists
1153         final ContentValues valuesOne = new ContentValues();
1154         valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1155         valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1156         valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3");
1157         valuesOne.put(AudioColumns.TITLE, "Clocks");
1158         valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood");
1159         valuesOne.put(AudioColumns.GENRE, "Rock");
1160         valuesOne.put(AudioColumns.IS_MUSIC, true);
1161 
1162         final ContentValues valuesTwo = new ContentValues();
1163         valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1164         valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1165         valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3");
1166         valuesTwo.put(AudioColumns.TITLE, "Sounds");
1167         valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood");
1168         valuesTwo.put(AudioColumns.GENRE, "Alternative rock");
1169         valuesTwo.put(AudioColumns.IS_MUSIC, true);
1170 
1171         MediaProvider.computeAudioKeyValues(valuesOne);
1172         final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID);
1173         MediaProvider.computeAudioKeyValues(valuesTwo);
1174         final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID);
1175 
1176         assertEquals(albumIdOne, albumIdTwo);
1177 
1178         // same album name, same album artists, different artists
1179         final ContentValues valuesThree = new ContentValues();
1180         valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1181         valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1182         valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3");
1183         valuesThree.put(AudioColumns.TITLE, "Silent");
1184         valuesThree.put(AudioColumns.ALBUM, "Rainbow");
1185         valuesThree.put(AudioColumns.ALBUM_ARTIST, "Various Artists");
1186         valuesThree.put(AudioColumns.ARTIST, "Sample1");
1187         valuesThree.put(AudioColumns.GENRE, "Rock");
1188         valuesThree.put(AudioColumns.IS_MUSIC, true);
1189 
1190         final ContentValues valuesFour = new ContentValues();
1191         valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
1192         valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY);
1193         valuesFour.put(FileColumns.DATA, "/storage/emulated/0/Rainbow.mp3");
1194         valuesFour.put(AudioColumns.TITLE, "Rainbow");
1195         valuesFour.put(AudioColumns.ALBUM, "Rainbow");
1196         valuesFour.put(AudioColumns.ALBUM_ARTIST, "Various Artists");
1197         valuesFour.put(AudioColumns.ARTIST, "Sample2");
1198         valuesFour.put(AudioColumns.GENRE, "Alternative rock");
1199         valuesFour.put(AudioColumns.IS_MUSIC, true);
1200 
1201         MediaProvider.computeAudioKeyValues(valuesThree);
1202         final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID);
1203         MediaProvider.computeAudioKeyValues(valuesFour);
1204         final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID);
1205 
1206         assertEquals(albumIdThree, albumIdFour);
1207     }
1208 
1209     @Test
testQueryAudioViewsNoTrashedItem()1210     public void testQueryAudioViewsNoTrashedItem() throws Exception {
1211         testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_TRASHED);
1212     }
1213 
1214     @Test
testQueryAudioViewsNoPendingItem()1215     public void testQueryAudioViewsNoPendingItem() throws Exception {
1216         testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_PENDING);
1217     }
1218 
testQueryAudioViewsNoItemWithColumn(String columnKey)1219     private void testQueryAudioViewsNoItemWithColumn(String columnKey) throws Exception {
1220         // We might have old files lurking, so force a clean slate
1221         final Context context = InstrumentationRegistry.getTargetContext();
1222         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
1223         sIsolatedResolver = sIsolatedContext.getContentResolver();
1224         final File dir = Environment
1225                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
1226 
1227         final File audio = new File(dir, "test" + System.nanoTime() + ".mp3");
1228         final Uri audioUri =
1229                 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
1230         final String album = "TestAlbum" + System.nanoTime();
1231         final String artist = "TestArtist" + System.nanoTime();
1232         final String genre = "TestGenre" + System.nanoTime();
1233         final String relativePath = extractRelativePath(audio.getAbsolutePath());
1234         final String displayName = extractDisplayName(audio.getAbsolutePath());
1235         ContentValues values = new ContentValues();
1236 
1237         values.put(MediaStore.Audio.Media.ALBUM, album);
1238         values.put(MediaStore.Audio.Media.ARTIST, artist);
1239         values.put(MediaStore.Audio.Media.GENRE, genre);
1240         values.put(MediaStore.Audio.Media.DISPLAY_NAME, displayName);
1241         values.put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath);
1242         values.put(MediaStore.Audio.Media.IS_MUSIC, 1);
1243         values.put(columnKey, 1);
1244 
1245         Uri result = sIsolatedResolver.insert(audioUri, values);
1246 
1247         // Check the audio file is inserted correctly
1248         try (Cursor c = sIsolatedResolver.query(result,
1249                 new String[]{MediaColumns.DISPLAY_NAME, columnKey}, null, null)) {
1250             assertNotNull(c);
1251             assertEquals(1, c.getCount());
1252             assertTrue(c.moveToFirst());
1253             assertEquals(displayName, c.getString(0));
1254             assertEquals(1, c.getInt(1));
1255         }
1256 
1257         final String volume = MediaStore.VOLUME_EXTERNAL_PRIMARY;
1258         assertQueryResultNoItems(MediaStore.Audio.Albums.getContentUri(volume));
1259         assertQueryResultNoItems(MediaStore.Audio.Artists.getContentUri(volume));
1260         assertQueryResultNoItems(MediaStore.Audio.Genres.getContentUri(volume));
1261     }
1262 
assertQueryResultNoItems(Uri uri)1263     private static void assertQueryResultNoItems(Uri uri) throws Exception {
1264         try (Cursor c = sIsolatedResolver.query(uri, null, null, null, null)) {
1265             assertNotNull(c);
1266             assertEquals(0, c.getCount());
1267         }
1268     }
1269 
assertRelativePathForDirectory(String directoryPath, String relativePath)1270     private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
1271         assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
1272                 .that(extractRelativePathForDirectory(directoryPath))
1273                 .isEqualTo(relativePath);
1274     }
1275 
computeDataValues(String path)1276     private static ContentValues computeDataValues(String path) {
1277         final ContentValues values = new ContentValues();
1278         values.put(MediaColumns.DATA, path);
1279         FileUtils.computeValuesFromData(values, /*forFuse*/ false);
1280         Log.v(TAG, "Computed values " + values);
1281         return values;
1282     }
1283 
assertBucket(ContentValues values, String bucketId, String bucketName)1284     private static void assertBucket(ContentValues values, String bucketId, String bucketName) {
1285         if (bucketId != null) {
1286             assertEquals(bucketName,
1287                     values.getAsString(ImageColumns.BUCKET_DISPLAY_NAME));
1288             assertEquals(bucketId.toLowerCase(Locale.ROOT).hashCode(),
1289                     (long) values.getAsLong(ImageColumns.BUCKET_ID));
1290         } else {
1291             assertNull(values.get(ImageColumns.BUCKET_DISPLAY_NAME));
1292             assertNull(values.get(ImageColumns.BUCKET_ID));
1293         }
1294     }
1295 
assertVolume(ContentValues values, String volumeName)1296     private static void assertVolume(ContentValues values, String volumeName) {
1297         assertEquals(volumeName, values.getAsString(ImageColumns.VOLUME_NAME));
1298     }
1299 
assertRelativePath(ContentValues values, String relativePath)1300     private static void assertRelativePath(ContentValues values, String relativePath) {
1301         assertEquals(relativePath, values.get(ImageColumns.RELATIVE_PATH));
1302     }
1303 
assertMimetype(ContentValues values, String type)1304     private static void assertMimetype(ContentValues values, String type) {
1305         assertEquals(type, values.get(MediaColumns.MIME_TYPE));
1306     }
1307 
assertDisplayName(ContentValues values, String type)1308     private static void assertDisplayName(ContentValues values, String type) {
1309         assertEquals(type, values.get(MediaColumns.DISPLAY_NAME));
1310     }
1311 
isGreylistMatch(String raw)1312     private static boolean isGreylistMatch(String raw) {
1313         for (Pattern p : MediaProvider.sGreylist) {
1314             if (p.matcher(raw).matches()) {
1315                 return true;
1316             }
1317         }
1318         return false;
1319     }
1320 
buildFile(Uri uri, String relativePath, String displayName, String mimeType)1321     private String buildFile(Uri uri, String relativePath, String displayName,
1322             String mimeType) {
1323         final ContentValues values = new ContentValues();
1324         if (relativePath != null) {
1325             values.put(MediaColumns.RELATIVE_PATH, relativePath);
1326         }
1327         values.put(MediaColumns.DISPLAY_NAME, displayName);
1328         values.put(MediaColumns.MIME_TYPE, mimeType);
1329         try {
1330             ensureFileColumns(uri, values);
1331         } catch (VolumeArgumentException | VolumeNotFoundException e) {
1332             throw e.rethrowAsIllegalArgumentException();
1333         }
1334         return values.getAsString(MediaColumns.DATA);
1335     }
1336 
ensureFileColumns(Uri uri, ContentValues values)1337     private void ensureFileColumns(Uri uri, ContentValues values)
1338             throws VolumeArgumentException, VolumeNotFoundException {
1339         try (ContentProviderClient cpc = sIsolatedResolver
1340                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1341             ((MediaProvider) cpc.getLocalContentProvider())
1342                     .ensureFileColumns(uri, values);
1343         }
1344     }
1345 
assertEndsWith(String expected, String actual)1346     private static void assertEndsWith(String expected, String actual) {
1347         if (!actual.endsWith(expected)) {
1348             fail("Expected ends with " + expected + " but found " + actual);
1349         }
1350     }
1351 
assertThrows(Class<T> clazz, Runnable r)1352     private static <T extends Exception> void assertThrows(Class<T> clazz, Runnable r) {
1353         try {
1354             r.run();
1355             fail("Expected " + clazz + " to be thrown");
1356         } catch (Exception e) {
1357             if (!clazz.isAssignableFrom(e.getClass())) {
1358                 throw e;
1359             }
1360         }
1361     }
1362 
1363     @Test
testNestedTransaction_applyBatch()1364     public void testNestedTransaction_applyBatch() throws Exception {
1365         final Uri[] uris = new Uri[]{
1366                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, 0),
1367                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 0),
1368         };
1369         final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
1370         ops.add(ContentProviderOperation.newDelete(uris[0]).build());
1371         ops.add(ContentProviderOperation.newDelete(uris[1]).build());
1372         sIsolatedResolver.applyBatch(MediaStore.AUTHORITY, ops);
1373     }
1374 
1375     @Test
testRedactionForInvalidUris()1376     public void testRedactionForInvalidUris() throws Exception {
1377         try (ContentProviderClient cpc = sIsolatedResolver
1378                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1379             MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider();
1380             final String volumeName = MediaStore.VOLUME_EXTERNAL;
1381             assertNull(mp.getRedactedUri(MediaStore.Images.Media.getContentUri(volumeName)));
1382             assertNull(mp.getRedactedUri(MediaStore.Video.Media.getContentUri(volumeName)));
1383             assertNull(mp.getRedactedUri(MediaStore.Audio.Media.getContentUri(volumeName)));
1384             assertNull(mp.getRedactedUri(MediaStore.Audio.Albums.getContentUri(volumeName)));
1385             assertNull(mp.getRedactedUri(MediaStore.Audio.Artists.getContentUri(volumeName)));
1386             assertNull(mp.getRedactedUri(MediaStore.Audio.Genres.getContentUri(volumeName)));
1387             assertNull(mp.getRedactedUri(MediaStore.Audio.Playlists.getContentUri(volumeName)));
1388             assertNull(mp.getRedactedUri(MediaStore.Downloads.getContentUri(volumeName)));
1389             assertNull(mp.getRedactedUri(MediaStore.Files.getContentUri(volumeName)));
1390 
1391             // Check with a very large value - which shouldn't be present normally (at least for
1392             // tests).
1393             assertNull(mp.getRedactedUri(
1394                     MediaStore.Images.Media.getContentUri(volumeName, Long.MAX_VALUE)));
1395         }
1396     }
1397 
1398     @Test
testRedactionForInvalidAndValidUris()1399     public void testRedactionForInvalidAndValidUris() throws Exception {
1400         final String volumeName = MediaStore.VOLUME_EXTERNAL;
1401         final List<Uri> uris = new ArrayList<>();
1402         uris.add(MediaStore.Images.Media.getContentUri(volumeName));
1403         uris.add(MediaStore.Video.Media.getContentUri(volumeName));
1404 
1405         final File dir = Environment
1406                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
1407         final File[] files = new File[]{
1408                 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")),
1409                 stage(R.raw.test_video_xmp,
1410                         new File(dir, "test" + System.nanoTime() + ".mp4")),
1411                 stage(R.raw.lg_g4_iso_800_jpg,
1412                         new File(dir, "test" + System.nanoTime() + ".jpg"))
1413         };
1414 
1415         try (ContentProviderClient cpc = sIsolatedResolver
1416                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1417             MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider();
1418             for (File file : files) {
1419                 uris.add(MediaStore.scanFile(sIsolatedResolver, file));
1420             }
1421 
1422             List<Uri> redactedUris = mp.getRedactedUri(uris);
1423             assertEquals(uris.size(), redactedUris.size());
1424             assertNull(redactedUris.get(0));
1425             assertNull(redactedUris.get(1));
1426             assertNotNull(redactedUris.get(2));
1427             assertNotNull(redactedUris.get(3));
1428             assertNotNull(redactedUris.get(4));
1429         } finally {
1430             for (File file : files) {
1431                 file.delete();
1432             }
1433         }
1434     }
1435 
1436     @Test
testRedactionForFileExtension()1437     public void testRedactionForFileExtension() throws Exception {
1438         testRedactionForFileExtension(R.raw.test_audio, ".mp3");
1439         testRedactionForFileExtension(R.raw.test_video_xmp, ".mp4");
1440         testRedactionForFileExtension(R.raw.lg_g4_iso_800_jpg, ".jpg");
1441     }
1442 
testRedactionForFileExtension(int resId, String extension)1443     private void testRedactionForFileExtension(int resId, String extension) throws Exception {
1444         final File dir = Environment
1445                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
1446         final File file = new File(dir, "test" + System.nanoTime() + extension);
1447 
1448         stage(resId, file);
1449 
1450         final List<Uri> uris = new ArrayList<>();
1451         uris.add(MediaStore.scanFile(sIsolatedResolver, file));
1452 
1453 
1454         try (ContentProviderClient cpc = sIsolatedResolver
1455                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1456             final MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider();
1457 
1458             final String[] projection = new String[]{MediaColumns.DISPLAY_NAME, MediaColumns.DATA};
1459             for (Uri uri : mp.getRedactedUri(uris)) {
1460                 try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) {
1461                     assertNotNull(c);
1462                     assertEquals(1, c.getCount());
1463                     assertTrue(c.moveToFirst());
1464                     assertTrue(c.getString(0).endsWith(extension));
1465                     assertTrue(c.getString(1).endsWith(extension));
1466                 }
1467             }
1468         } finally {
1469             file.delete();
1470         }
1471     }
1472 }
1473