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