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