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.extractRelativePathForDirectory;
21 import static com.android.providers.media.util.FileUtils.isDownload;
22 import static com.android.providers.media.util.FileUtils.isDownloadDir;
23 
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static org.junit.Assert.assertArrayEquals;
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertFalse;
29 import static org.junit.Assert.assertNotNull;
30 import static org.junit.Assert.assertNull;
31 import static org.junit.Assert.assertTrue;
32 import static org.junit.Assert.fail;
33 
34 import android.Manifest;
35 import android.content.ContentProviderClient;
36 import android.content.ContentProviderOperation;
37 import android.content.ContentResolver;
38 import android.content.ContentUris;
39 import android.content.ContentValues;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.pm.PackageManager;
43 import android.database.Cursor;
44 import android.net.Uri;
45 import android.os.Build;
46 import android.os.Bundle;
47 import android.os.CancellationSignal;
48 import android.os.Environment;
49 import android.provider.MediaStore;
50 import android.provider.MediaStore.Images.ImageColumns;
51 import android.provider.MediaStore.MediaColumns;
52 import android.util.ArrayMap;
53 import android.util.Log;
54 import android.util.Pair;
55 
56 import androidx.test.InstrumentationRegistry;
57 import androidx.test.runner.AndroidJUnit4;
58 
59 import com.android.providers.media.MediaProvider.FallbackException;
60 import com.android.providers.media.MediaProvider.VolumeArgumentException;
61 import com.android.providers.media.MediaProvider.VolumeNotFoundException;
62 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
63 import com.android.providers.media.util.FileUtils;
64 import com.android.providers.media.util.SQLiteQueryBuilder;
65 
66 import org.junit.AfterClass;
67 import org.junit.Assume;
68 import org.junit.BeforeClass;
69 import org.junit.Ignore;
70 import org.junit.Test;
71 import org.junit.runner.RunWith;
72 
73 import java.io.ByteArrayOutputStream;
74 import java.io.File;
75 import java.io.PrintWriter;
76 import java.util.ArrayList;
77 import java.util.Arrays;
78 import java.util.Collection;
79 import java.util.Collections;
80 import java.util.Locale;
81 import java.util.regex.Pattern;
82 
83 @RunWith(AndroidJUnit4.class)
84 public class MediaProviderTest {
85     static final String TAG = "MediaProviderTest";
86 
87     /**
88      * To confirm behaviors, we need to pick an app installed on all devices
89      * which has no permissions, and the best candidate is the "Easter Egg" app.
90      */
91     static final String PERMISSIONLESS_APP = "com.android.egg";
92 
93     private static Context sIsolatedContext;
94     private static ContentResolver sIsolatedResolver;
95 
96     @BeforeClass
setUp()97     public static void setUp() {
98         InstrumentationRegistry.getInstrumentation().getUiAutomation()
99                 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
100                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
101 
102         final Context context = InstrumentationRegistry.getTargetContext();
103         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
104         sIsolatedResolver = sIsolatedContext.getContentResolver();
105     }
106 
107     @AfterClass
tearDown()108     public static void tearDown() {
109         InstrumentationRegistry.getInstrumentation()
110                 .getUiAutomation().dropShellPermissionIdentity();
111     }
112 
113     /**
114      * To fully exercise all our tests, we require that the Cuttlefish emulator
115      * have both emulated primary storage and an SD card be present.
116      */
117     @Test
testCuttlefish()118     public void testCuttlefish() {
119         Assume.assumeTrue(Build.MODEL.contains("Cuttlefish"));
120 
121         assertTrue("Cuttlefish must have both emulated storage and an SD card to exercise tests",
122                 MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext())
123                         .size() > 1);
124     }
125 
126     @Test
testSchema()127     public void testSchema() {
128         for (String path : new String[] {
129                 "images/media",
130                 "images/media/1",
131                 "images/thumbnails",
132                 "images/thumbnails/1",
133 
134                 "audio/media",
135                 "audio/media/1",
136                 "audio/media/1/genres",
137                 "audio/media/1/genres/1",
138                 "audio/genres",
139                 "audio/genres/1",
140                 "audio/genres/1/members",
141                 "audio/playlists",
142                 "audio/playlists/1",
143                 "audio/playlists/1/members",
144                 "audio/playlists/1/members/1",
145                 "audio/artists",
146                 "audio/artists/1",
147                 "audio/artists/1/albums",
148                 "audio/albums",
149                 "audio/albums/1",
150                 "audio/albumart",
151                 "audio/albumart/1",
152 
153                 "video/media",
154                 "video/media/1",
155                 "video/thumbnails",
156                 "video/thumbnails/1",
157 
158                 "file",
159                 "file/1",
160 
161                 "downloads",
162                 "downloads/1",
163         }) {
164             final Uri probe = MediaStore.AUTHORITY_URI.buildUpon()
165                     .appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build();
166             try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) {
167                 assertNotNull("probe", c);
168             }
169             try {
170                 sIsolatedResolver.getType(probe);
171             } catch (IllegalStateException tolerated) {
172             }
173         }
174     }
175 
176     @Test
testLocale()177     public void testLocale() {
178         try (ContentProviderClient cpc = sIsolatedResolver
179                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
180             ((MediaProvider) cpc.getLocalContentProvider())
181                     .onLocaleChanged();
182         }
183     }
184 
185     @Test
testDump()186     public void testDump() throws Exception {
187         try (ContentProviderClient cpc = sIsolatedResolver
188                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
189             cpc.getLocalContentProvider().dump(null,
190                     new PrintWriter(new ByteArrayOutputStream()), null);
191         }
192     }
193 
194     /**
195      * Verify that our fallback exceptions throw on modern apps while degrading
196      * gracefully for legacy apps.
197      */
198     @Test
testFallbackException()199     public void testFallbackException() throws Exception {
200         for (FallbackException e : new FallbackException[] {
201                 new FallbackException("test", Build.VERSION_CODES.Q),
202                 new VolumeNotFoundException("test"),
203                 new VolumeArgumentException(new File("/"), Collections.emptyList())
204         }) {
205             // Modern apps should get thrown
206             assertThrows(Exception.class, () -> {
207                 e.translateForInsert(Build.VERSION_CODES.CUR_DEVELOPMENT);
208             });
209             assertThrows(Exception.class, () -> {
210                 e.translateForUpdateDelete(Build.VERSION_CODES.CUR_DEVELOPMENT);
211             });
212             assertThrows(Exception.class, () -> {
213                 e.translateForQuery(Build.VERSION_CODES.CUR_DEVELOPMENT);
214             });
215 
216             // Legacy apps gracefully log without throwing
217             assertEquals(null, e.translateForInsert(Build.VERSION_CODES.BASE));
218             assertEquals(0, e.translateForUpdateDelete(Build.VERSION_CODES.BASE));
219             assertEquals(null, e.translateForQuery(Build.VERSION_CODES.BASE));
220         }
221     }
222 
223     /**
224      * We already have solid coverage of this logic in {@link IdleServiceTest},
225      * but the coverage system currently doesn't measure that, so we add the
226      * bare minimum local testing here to convince the tooling that it's
227      * covered.
228      */
229     @Test
testIdle()230     public void testIdle() throws Exception {
231         try (ContentProviderClient cpc = sIsolatedResolver
232                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
233             ((MediaProvider) cpc.getLocalContentProvider())
234                     .onIdleMaintenance(new CancellationSignal());
235         }
236     }
237 
238     /**
239      * We already have solid coverage of this logic in
240      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
241      * measure that, so we add the bare minimum local testing here to convince
242      * the tooling that it's covered.
243      */
244     @Test
testCanonicalize()245     public void testCanonicalize() throws Exception {
246         // We might have old files lurking, so force a clean slate
247         final Context context = InstrumentationRegistry.getTargetContext();
248         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
249         sIsolatedResolver = sIsolatedContext.getContentResolver();
250 
251         final File dir = Environment
252                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
253         for (File file : new File[] {
254                 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")),
255                 stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")),
256                 stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg"))
257         }) {
258             final Uri uri = MediaStore.scanFile(sIsolatedResolver, file);
259             Log.v(TAG, "Scanned " + file + " as " + uri);
260 
261             final Uri forward = sIsolatedResolver.canonicalize(uri);
262             final Uri reverse = sIsolatedResolver.uncanonicalize(forward);
263 
264             assertEquals(ContentUris.parseId(uri), ContentUris.parseId(forward));
265             assertEquals(ContentUris.parseId(uri), ContentUris.parseId(reverse));
266         }
267     }
268 
269     /**
270      * We already have solid coverage of this logic in
271      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
272      * measure that, so we add the bare minimum local testing here to convince
273      * the tooling that it's covered.
274      */
275     @Test
testMetadata()276     public void testMetadata() {
277         assertNotNull(MediaStore.getVersion(sIsolatedContext,
278                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
279         assertNotNull(MediaStore.getGeneration(sIsolatedResolver,
280                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
281     }
282 
283     /**
284      * We already have solid coverage of this logic in
285      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
286      * measure that, so we add the bare minimum local testing here to convince
287      * the tooling that it's covered.
288      */
289     @Test
testCreateRequest()290     public void testCreateRequest() throws Exception {
291         final Collection<Uri> uris = Arrays.asList(
292                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42));
293         assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris));
294     }
295 
296     /**
297      * We already have solid coverage of this logic in
298      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
299      * measure that, so we add the bare minimum local testing here to convince
300      * the tooling that it's covered.
301      */
302     @Test
testCheckUriPermission()303     public void testCheckUriPermission() throws Exception {
304         final ContentValues values = new ContentValues();
305         values.put(MediaColumns.DISPLAY_NAME, "test.mp3");
306         values.put(MediaColumns.MIME_TYPE, "audio/mpeg");
307         final Uri uri = sIsolatedResolver.insert(
308                 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
309 
310         assertEquals(PackageManager.PERMISSION_GRANTED, sIsolatedResolver.checkUriPermission(uri,
311                 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
312     }
313 
314     /**
315      * We already have solid coverage of this logic in
316      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
317      * measure that, so we add the bare minimum local testing here to convince
318      * the tooling that it's covered.
319      */
320     @Test
testBulkInsert()321     public void testBulkInsert() throws Exception {
322         final ContentValues values1 = new ContentValues();
323         values1.put(MediaColumns.DISPLAY_NAME, "test1.mp3");
324         values1.put(MediaColumns.MIME_TYPE, "audio/mpeg");
325 
326         final ContentValues values2 = new ContentValues();
327         values2.put(MediaColumns.DISPLAY_NAME, "test2.mp3");
328         values2.put(MediaColumns.MIME_TYPE, "audio/mpeg");
329 
330         final Uri targetUri = MediaStore.Audio.Media
331                 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
332         assertEquals(2, sIsolatedResolver.bulkInsert(targetUri,
333                 new ContentValues[] { values1, values2 }));
334     }
335 
336     /**
337      * We already have solid coverage of this logic in
338      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
339      * measure that, so we add the bare minimum local testing here to convince
340      * the tooling that it's covered.
341      */
342     @Test
testCustomCollator()343     public void testCustomCollator() throws Exception {
344         final Bundle extras = new Bundle();
345         extras.putString(ContentResolver.QUERY_ARG_SORT_LOCALE, "en");
346 
347         try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI,
348                 null, extras, null)) {
349             assertNotNull(c);
350         }
351     }
352 
353     /**
354      * We already have solid coverage of this logic in
355      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
356      * measure that, so we add the bare minimum local testing here to convince
357      * the tooling that it's covered.
358      */
359     @Test
testGetRedactionRanges_Image()360     public void testGetRedactionRanges_Image() throws Exception {
361         final File file = File.createTempFile("test", ".jpg");
362         stage(R.raw.test_image, file);
363         assertNotNull(MediaProvider.getRedactionRanges(file));
364     }
365 
366     /**
367      * We already have solid coverage of this logic in
368      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
369      * measure that, so we add the bare minimum local testing here to convince
370      * the tooling that it's covered.
371      */
372     @Test
testGetRedactionRanges_Video()373     public void testGetRedactionRanges_Video() throws Exception {
374         final File file = File.createTempFile("test", ".mp4");
375         stage(R.raw.test_video, file);
376         assertNotNull(MediaProvider.getRedactionRanges(file));
377     }
378 
379     @Test
testComputeCommonPrefix_Single()380     public void testComputeCommonPrefix_Single() {
381         assertEquals(Uri.parse("content://authority/1/2/3"),
382                 MediaProvider.computeCommonPrefix(Arrays.asList(
383                         Uri.parse("content://authority/1/2/3"))));
384     }
385 
386     @Test
testComputeCommonPrefix_Deeper()387     public void testComputeCommonPrefix_Deeper() {
388         assertEquals(Uri.parse("content://authority/1/2/3"),
389                 MediaProvider.computeCommonPrefix(Arrays.asList(
390                         Uri.parse("content://authority/1/2/3/4"),
391                         Uri.parse("content://authority/1/2/3/4/5"),
392                         Uri.parse("content://authority/1/2/3"))));
393     }
394 
395     @Test
testComputeCommonPrefix_Siblings()396     public void testComputeCommonPrefix_Siblings() {
397         assertEquals(Uri.parse("content://authority/1/2"),
398                 MediaProvider.computeCommonPrefix(Arrays.asList(
399                         Uri.parse("content://authority/1/2/3"),
400                         Uri.parse("content://authority/1/2/99"))));
401     }
402 
403     @Test
testComputeCommonPrefix_Drastic()404     public void testComputeCommonPrefix_Drastic() {
405         assertEquals(Uri.parse("content://authority"),
406                 MediaProvider.computeCommonPrefix(Arrays.asList(
407                         Uri.parse("content://authority/1/2/3"),
408                         Uri.parse("content://authority/99/99/99"))));
409     }
410 
getPathOwnerPackageName(String path)411     private static String getPathOwnerPackageName(String path) {
412         return FileUtils.extractPathOwnerPackageName(path);
413     }
414 
415     @Test
testPathOwnerPackageName_None()416     public void testPathOwnerPackageName_None() throws Exception {
417         assertEquals(null, getPathOwnerPackageName(null));
418         assertEquals(null, getPathOwnerPackageName("/data/path"));
419     }
420 
421     @Test
testPathOwnerPackageName_Emulated()422     public void testPathOwnerPackageName_Emulated() throws Exception {
423         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/DCIM/foo.jpg"));
424         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/"));
425         assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/data/"));
426 
427         assertEquals("com.example",
428                 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/"));
429         assertEquals("com.example",
430                 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/foo.jpg"));
431         assertEquals("com.example",
432                 getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg"));
433         assertEquals("com.example",
434                 getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg"));
435         assertEquals("com.example",
436                 getPathOwnerPackageName("/storage/emulated/0/Android/sandbox/com.example/foo.jpg"));
437     }
438 
439     @Test
testPathOwnerPackageName_Portable()440     public void testPathOwnerPackageName_Portable() throws Exception {
441         assertEquals(null, getPathOwnerPackageName("/storage/0000-0000/DCIM/foo.jpg"));
442 
443         assertEquals("com.example",
444                 getPathOwnerPackageName("/storage/0000-0000/Android/data/com.example/foo.jpg"));
445     }
446 
447     @Test
testBuildData_Simple()448     public void testBuildData_Simple() throws Exception {
449         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
450         assertEndsWith("/Pictures/file.png",
451                 buildFile(uri, null, "file", "image/png"));
452         assertEndsWith("/Pictures/file.png",
453                 buildFile(uri, null, "file.png", "image/png"));
454         assertEndsWith("/Pictures/file.jpg.png",
455                 buildFile(uri, null, "file.jpg", "image/png"));
456     }
457 
458     @Test
testBuildData_Primary()459     public void testBuildData_Primary() throws Exception {
460         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
461         assertEndsWith("/DCIM/IMG_1024.JPG",
462                 buildFile(uri, Environment.DIRECTORY_DCIM, "IMG_1024.JPG", "image/jpeg"));
463     }
464 
465     @Test
466     @Ignore("Enable as part of b/142561358")
testBuildData_Secondary()467     public void testBuildData_Secondary() throws Exception {
468         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
469         assertEndsWith("/Pictures/Screenshots/foo.png",
470                 buildFile(uri, "Pictures/Screenshots", "foo.png", "image/png"));
471     }
472 
473     @Test
testBuildData_InvalidNames()474     public void testBuildData_InvalidNames() throws Exception {
475         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
476         assertEndsWith("/Pictures/foo_bar.png",
477             buildFile(uri, null, "foo/bar", "image/png"));
478         assertEndsWith("/Pictures/_.hidden.png",
479             buildFile(uri, null, ".hidden", "image/png"));
480     }
481 
482     @Test
testBuildData_InvalidTypes()483     public void testBuildData_InvalidTypes() throws Exception {
484         for (String type : new String[] {
485                 "audio/foo", "video/foo", "image/foo", "application/foo", "foo/foo"
486         }) {
487             if (!type.startsWith("audio/")) {
488                 assertThrows(IllegalArgumentException.class, () -> {
489                     buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
490                             null, "foo", type);
491                 });
492             }
493             if (!type.startsWith("video/")) {
494                 assertThrows(IllegalArgumentException.class, () -> {
495                     buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
496                             null, "foo", type);
497                 });
498             }
499             if (!type.startsWith("image/")) {
500                 assertThrows(IllegalArgumentException.class, () -> {
501                     buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
502                             null, "foo", type);
503                 });
504             }
505         }
506     }
507 
508     @Test
testBuildData_InvalidSecondaryTypes()509     public void testBuildData_InvalidSecondaryTypes() throws Exception {
510         assertEndsWith("/Pictures/foo.png",
511                 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
512                         null, "foo.png", "image/*"));
513 
514         assertThrows(IllegalArgumentException.class, () -> {
515             buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
516                     null, "foo", "video/*");
517         });
518         assertThrows(IllegalArgumentException.class, () -> {
519             buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
520                     null, "foo.mp4", "audio/*");
521         });
522     }
523 
524     @Test
testBuildData_EmptyTypes()525     public void testBuildData_EmptyTypes() throws Exception {
526         Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
527         assertEndsWith("/Pictures/foo.png",
528                 buildFile(uri, null, "foo.png", ""));
529 
530         uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
531         assertEndsWith(".mp4",
532                 buildFile(uri, null, "", ""));
533     }
534 
535     @Test
testEnsureFileColumns_InvalidMimeType_targetSdkQ()536     public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception {
537         final MediaProvider provider = new MediaProvider() {
538             @Override
539             public boolean isFuseThread() {
540                 return false;
541             }
542 
543             @Override
544             public int getCallingPackageTargetSdkVersion() {
545                 return Build.VERSION_CODES.Q;
546             }
547         };
548 
549         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
550         final ContentValues values = new ContentValues();
551 
552         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
553         provider.ensureFileColumns(uri, values);
554         assertMimetype(values, "image/jpeg");
555         assertDisplayName(values, "pngimage.png.jpg");
556 
557         values.clear();
558         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
559         values.put(MediaColumns.MIME_TYPE, "");
560         provider.ensureFileColumns(uri, values);
561         assertMimetype(values, "image/jpeg");
562         assertDisplayName(values, "pngimage.png.jpg");
563 
564         values.clear();
565         values.put(MediaColumns.MIME_TYPE, "");
566         provider.ensureFileColumns(uri, values);
567         assertMimetype(values, "image/jpeg");
568 
569         values.clear();
570         values.put(MediaColumns.DISPLAY_NAME, "foo.foo");
571         provider.ensureFileColumns(uri, values);
572         assertMimetype(values, "image/jpeg");
573         assertDisplayName(values, "foo.foo.jpg");
574     }
575 
576     @Ignore("Enable as part of b/142561358")
testBuildData_Charset()577     public void testBuildData_Charset() throws Exception {
578         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
579         assertEndsWith("/Pictures/foo__bar/bar__baz.png",
580                 buildFile(uri, "Pictures/foo\0\0bar", "bar::baz.png", "image/png"));
581     }
582 
583     @Test
testBuildData_Playlists()584     public void testBuildData_Playlists() throws Exception {
585         final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
586         assertEndsWith("/Music/my_playlist.m3u",
587                 buildFile(uri, null, "my_playlist", "audio/mpegurl"));
588         assertEndsWith("/Movies/my_playlist.pls",
589                 buildFile(uri, "Movies", "my_playlist", "audio/x-scpls"));
590     }
591 
592     @Test
testBuildData_Subtitles()593     public void testBuildData_Subtitles() throws Exception {
594         final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
595         assertEndsWith("/Movies/my_subtitle.srt",
596                 buildFile(uri, null, "my_subtitle", "application/x-subrip"));
597         assertEndsWith("/Music/my_lyrics.lrc",
598                 buildFile(uri, "Music", "my_lyrics", "application/lrc"));
599     }
600 
601     @Test
testBuildData_Downloads()602     public void testBuildData_Downloads() throws Exception {
603         final Uri uri = MediaStore.Downloads
604                 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
605         assertEndsWith("/Download/linux.iso",
606                 buildFile(uri, null, "linux.iso", "application/x-iso9660-image"));
607     }
608 
609     @Test
testBuildData_Pending_FromValues()610     public void testBuildData_Pending_FromValues() throws Exception {
611         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
612         final ContentValues forward = new ContentValues();
613         forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
614         forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
615         forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
616         forward.put(MediaColumns.IS_PENDING, 1);
617         forward.put(MediaColumns.IS_TRASHED, 0);
618         forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
619         ensureFileColumns(uri, forward);
620 
621         // Requested filename remains intact, but raw path on disk is mutated to
622         // reflect that it's a pending item with a specific expiration time
623         assertEquals("IMG1024.JPG",
624                 forward.getAsString(MediaColumns.DISPLAY_NAME));
625         assertEndsWith(".pending-1577836800-IMG1024.JPG",
626                 forward.getAsString(MediaColumns.DATA));
627     }
628 
629     @Test
testBuildData_Pending_FromData()630     public void testBuildData_Pending_FromData() throws Exception {
631         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
632         final ContentValues reverse = new ContentValues();
633         reverse.put(MediaColumns.DATA,
634                 "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG");
635         ensureFileColumns(uri, reverse);
636 
637         assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
638         assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
639         assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
640         assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
641         assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
642         assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
643     }
644 
645     @Test
testBuildData_Trashed_FromValues()646     public void testBuildData_Trashed_FromValues() throws Exception {
647         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
648         final ContentValues forward = new ContentValues();
649         forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/");
650         forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG");
651         forward.put(MediaColumns.MIME_TYPE, "image/jpeg");
652         forward.put(MediaColumns.IS_PENDING, 0);
653         forward.put(MediaColumns.IS_TRASHED, 1);
654         forward.put(MediaColumns.DATE_EXPIRES, 1577836800L);
655         ensureFileColumns(uri, forward);
656 
657         // Requested filename remains intact, but raw path on disk is mutated to
658         // reflect that it's a trashed item with a specific expiration time
659         assertEquals("IMG1024.JPG",
660                 forward.getAsString(MediaColumns.DISPLAY_NAME));
661         assertEndsWith(".trashed-1577836800-IMG1024.JPG",
662                 forward.getAsString(MediaColumns.DATA));
663     }
664 
665     @Test
testBuildData_Trashed_FromData()666     public void testBuildData_Trashed_FromData() throws Exception {
667         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
668         final ContentValues reverse = new ContentValues();
669         reverse.put(MediaColumns.DATA,
670                 "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG");
671         ensureFileColumns(uri, reverse);
672 
673         assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH));
674         assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME));
675         assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE));
676         assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING));
677         assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED));
678         assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES));
679     }
680 
681     @Test
testGreylist()682     public void testGreylist() throws Exception {
683         assertFalse(isGreylistMatch(
684                 "SELECT secret FROM other_table"));
685 
686         assertTrue(isGreylistMatch(
687                 "case when case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end > case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end then case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end else case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end end as corrected_added_modified"));
688         assertTrue(isGreylistMatch(
689                 "MAX(case when (datetaken >= 157680000 and datetaken < 1892160000) then datetaken * 1000 when (datetaken >= 157680000000 and datetaken < 1892160000000) then datetaken when (datetaken >= 157680000000000 and datetaken < 1892160000000000) then datetaken / 1000 else 0 end)"));
690         assertTrue(isGreylistMatch(
691                 "0 as orientation"));
692         assertTrue(isGreylistMatch(
693                 "\"content://media/internal/audio/media\""));
694     }
695 
696     @Test
testGreylist_115845887()697     public void testGreylist_115845887() {
698         assertTrue(isGreylistMatch(
699                 "MAX(*)"));
700         assertTrue(isGreylistMatch(
701                 "MAX(_id)"));
702 
703         assertTrue(isGreylistMatch(
704                 "sum(column_name)"));
705         assertFalse(isGreylistMatch(
706                 "SUM(foo+bar)"));
707 
708         assertTrue(isGreylistMatch(
709                 "count(column_name)"));
710         assertFalse(isGreylistMatch(
711                 "count(other_table.column_name)"));
712     }
713 
714     @Test
testGreylist_116489751_116135586_116117120_116084561_116074030_116062802()715     public void testGreylist_116489751_116135586_116117120_116084561_116074030_116062802() {
716         assertTrue(isGreylistMatch(
717                 "MAX(case when (date_added >= 157680000 and date_added < 1892160000) then date_added * 1000 when (date_added >= 157680000000 and date_added < 1892160000000) then date_added when (date_added >= 157680000000000 and date_added < 1892160000000000) then date_added / 1000 else 0 end)"));
718     }
719 
720     @Test
testGreylist_116699470()721     public void testGreylist_116699470() {
722         assertTrue(isGreylistMatch(
723                 "MAX(case when (date_modified >= 157680000 and date_modified < 1892160000) then date_modified * 1000 when (date_modified >= 157680000000 and date_modified < 1892160000000) then date_modified when (date_modified >= 157680000000000 and date_modified < 1892160000000000) then date_modified / 1000 else 0 end)"));
724     }
725 
726     @Test
testGreylist_116531759()727     public void testGreylist_116531759() {
728         assertTrue(isGreylistMatch(
729                 "count(*)"));
730         assertTrue(isGreylistMatch(
731                 "COUNT(*)"));
732         assertFalse(isGreylistMatch(
733                 "xCOUNT(*)"));
734         assertTrue(isGreylistMatch(
735                 "count(*) AS image_count"));
736         assertTrue(isGreylistMatch(
737                 "count(_id)"));
738         assertTrue(isGreylistMatch(
739                 "count(_id) AS image_count"));
740 
741         assertTrue(isGreylistMatch(
742                 "column_a AS column_b"));
743         assertFalse(isGreylistMatch(
744                 "other_table.column_a AS column_b"));
745     }
746 
747     @Test
testGreylist_118475754()748     public void testGreylist_118475754() {
749         assertTrue(isGreylistMatch(
750                 "count(*) pcount"));
751         assertTrue(isGreylistMatch(
752                 "foo AS bar"));
753         assertTrue(isGreylistMatch(
754                 "foo bar"));
755         assertTrue(isGreylistMatch(
756                 "count(foo) AS bar"));
757         assertTrue(isGreylistMatch(
758                 "count(foo) bar"));
759     }
760 
761     @Test
testGreylist_119522660()762     public void testGreylist_119522660() {
763         assertTrue(isGreylistMatch(
764                 "CAST(_id AS TEXT) AS string_id"));
765         assertTrue(isGreylistMatch(
766                 "cast(_id as text)"));
767     }
768 
769     @Test
testGreylist_126945991()770     public void testGreylist_126945991() {
771         assertTrue(isGreylistMatch(
772                 "substr(_data, length(_data)-length(_display_name), 1) as filename_prevchar"));
773     }
774 
775     @Test
testGreylist_127900881()776     public void testGreylist_127900881() {
777         assertTrue(isGreylistMatch(
778                 "*"));
779     }
780 
781     @Test
testGreylist_128389972()782     public void testGreylist_128389972() {
783         assertTrue(isGreylistMatch(
784                 " count(bucket_id) images_count"));
785     }
786 
787     @Test
testGreylist_129746861()788     public void testGreylist_129746861() {
789         assertTrue(isGreylistMatch(
790                 "case when (datetaken >= 157680000 and datetaken < 1892160000) then datetaken * 1000 when (datetaken >= 157680000000 and datetaken < 1892160000000) then datetaken when (datetaken >= 157680000000000 and datetaken < 1892160000000000) then datetaken / 1000 else 0 end"));
791     }
792 
793     @Test
testGreylist_114112523()794     public void testGreylist_114112523() {
795         assertTrue(isGreylistMatch(
796                 "audio._id AS _id"));
797     }
798 
799     @Test
testComputeProjection_AggregationAllowed()800     public void testComputeProjection_AggregationAllowed() throws Exception {
801         final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
802         final ArrayMap<String, String> map = new ArrayMap<>();
803         map.put("external", "internal");
804         builder.setProjectionMap(map);
805         builder.setStrict(true);
806 
807         assertArrayEquals(
808                 new String[] { "internal" },
809                 builder.computeProjection(null));
810         assertArrayEquals(
811                 new String[] { "internal" },
812                 builder.computeProjection(new String[] { "external" }));
813         assertThrows(IllegalArgumentException.class, () -> {
814             builder.computeProjection(new String[] { "internal" });
815         });
816         assertThrows(IllegalArgumentException.class, () -> {
817             builder.computeProjection(new String[] { "MIN(internal)" });
818         });
819         assertArrayEquals(
820                 new String[] { "MIN(internal)" },
821                 builder.computeProjection(new String[] { "MIN(external)" }));
822         assertThrows(IllegalArgumentException.class, () -> {
823             builder.computeProjection(new String[] { "FOO(external)" });
824         });
825     }
826 
827     @Test
testIsDownload()828     public void testIsDownload() throws Exception {
829         assertTrue(isDownload("/storage/emulated/0/Download/colors.png"));
830         assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
831         assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4"));
832         assertTrue(isDownload("/storage/0000-0000/Download/foo.txt"));
833         assertTrue(isDownload(
834                 "/storage/emulated/0/Android/sandbox/com.example/Download/colors.png"));
835         assertTrue(isDownload(
836                 "/storage/emulated/0/Android/sandbox/shared-com.uid.shared/Download/colors.png"));
837         assertTrue(isDownload(
838                 "/storage/0000-0000/Android/sandbox/com.example/Download/colors.png"));
839         assertTrue(isDownload(
840                 "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download/colors.png"));
841 
842 
843         assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png"));
844         assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png"));
845         assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt"));
846         assertFalse(isDownload(
847                 "/storage/emulated/0/Android/sandbox/com.example/dir/Download/foo.txt"));
848         assertFalse(isDownload("/storage/emulated/0/Download"));
849         assertFalse(isDownload("/storage/emulated/0/Android/sandbox/com.example/Download"));
850         assertFalse(isDownload(
851                 "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download"));
852     }
853 
854     @Test
testIsDownloadDir()855     public void testIsDownloadDir() throws Exception {
856         assertTrue(isDownloadDir("/storage/emulated/0/Download"));
857         assertTrue(isDownloadDir("/storage/emulated/0/Android/sandbox/com.example/Download"));
858 
859         assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png"));
860         assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/"));
861         assertFalse(isDownloadDir(
862                 "/storage/emulated/0/Android/sandbox/com.example/Download/dir/foo.txt"));
863     }
864 
865     @Test
testComputeDataValues_Grouped()866     public void testComputeDataValues_Grouped() throws Exception {
867         for (String data : new String[] {
868                 "/storage/0000-0000/DCIM/Camera/IMG1024.JPG",
869                 "/storage/0000-0000/DCIM/Camera/iMg1024.JpG",
870                 "/storage/0000-0000/DCIM/Camera/IMG1024.CR2",
871                 "/storage/0000-0000/DCIM/Camera/IMG1024.BURST001.JPG",
872         }) {
873             final ContentValues values = computeDataValues(data);
874             assertVolume(values, "0000-0000");
875             assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
876             assertRelativePath(values, "DCIM/Camera/");
877         }
878     }
879 
880     @Test
testComputeDataValues_Extensions()881     public void testComputeDataValues_Extensions() throws Exception {
882         ContentValues values;
883 
884         values = computeDataValues("/storage/0000-0000/DCIM/Camera/IMG1024");
885         assertVolume(values, "0000-0000");
886         assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
887         assertRelativePath(values, "DCIM/Camera/");
888 
889         values = computeDataValues("/storage/0000-0000/DCIM/Camera/.foo");
890         assertVolume(values, "0000-0000");
891         assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera");
892         assertRelativePath(values, "DCIM/Camera/");
893 
894         values = computeDataValues("/storage/476A-17F8/123456/test.png");
895         assertVolume(values, "476a-17f8");
896         assertBucket(values, "/storage/476A-17F8/123456", "123456");
897         assertRelativePath(values, "123456/");
898 
899         values = computeDataValues("/storage/476A-17F8/123456/789/test.mp3");
900         assertVolume(values, "476a-17f8");
901         assertBucket(values, "/storage/476A-17F8/123456/789", "789");
902         assertRelativePath(values, "123456/789/");
903     }
904 
905     @Test
testComputeDataValues_DirectoriesInvalid()906     public void testComputeDataValues_DirectoriesInvalid() throws Exception {
907         for (String data : new String[] {
908                 "/storage/IMG1024.JPG",
909                 "/data/media/IMG1024.JPG",
910                 "IMG1024.JPG",
911         }) {
912             final ContentValues values = computeDataValues(data);
913             assertRelativePath(values, null);
914         }
915     }
916 
917     @Test
testComputeDataValues_Directories()918     public void testComputeDataValues_Directories() throws Exception {
919         ContentValues values;
920 
921         for (String top : new String[] {
922                 "/storage/emulated/0",
923                 "/storage/emulated/0/Android/sandbox/com.example",
924         }) {
925             values = computeDataValues(top + "/IMG1024.JPG");
926             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
927             assertBucket(values, top, null);
928             assertRelativePath(values, "/");
929 
930             values = computeDataValues(top + "/One/IMG1024.JPG");
931             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
932             assertBucket(values, top + "/One", "One");
933             assertRelativePath(values, "One/");
934 
935             values = computeDataValues(top + "/One/Two/IMG1024.JPG");
936             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
937             assertBucket(values, top + "/One/Two", "Two");
938             assertRelativePath(values, "One/Two/");
939 
940             values = computeDataValues(top + "/One/Two/Three/IMG1024.JPG");
941             assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY);
942             assertBucket(values, top + "/One/Two/Three", "Three");
943             assertRelativePath(values, "One/Two/Three/");
944         }
945     }
946 
947     @Test
testEnsureFileColumns_resolvesMimeType()948     public void testEnsureFileColumns_resolvesMimeType() throws Exception {
949         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
950         final ContentValues values = new ContentValues();
951         values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
952 
953         final MediaProvider provider = new MediaProvider() {
954             @Override
955             public boolean isFuseThread() {
956                 return false;
957             }
958 
959             @Override
960             public int getCallingPackageTargetSdkVersion() {
961                 return Build.VERSION_CODES.CUR_DEVELOPMENT;
962             }
963         };
964         provider.ensureFileColumns(uri, values);
965 
966         assertMimetype(values, "image/png");
967     }
968 
969     @Test
testRelativePathForInvalidDirectories()970     public void testRelativePathForInvalidDirectories() throws Exception {
971         for (String path : new String[] {
972                 "/storage/emulated",
973                 "/storage",
974                 "/data/media/Foo.jpg",
975                 "Foo.jpg",
976                 "storage/Foo"
977         }) {
978             assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
979         }
980     }
981 
982     @Test
testRelativePathForValidDirectories()983     public void testRelativePathForValidDirectories() throws Exception {
984         for (String prefix : new String[] {
985                 "/storage/emulated/0",
986                 "/storage/emulated/10",
987                 "/storage/ABCD-1234"
988         }) {
989             assertRelativePathForDirectory(prefix, "/");
990             assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/");
991             assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/");
992             assertRelativePathForDirectory(prefix + "/Z", "Z/");
993             assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo",
994                     "Android/media/com.example/Foo/");
995         }
996     }
997 
assertRelativePathForDirectory(String directoryPath, String relativePath)998     private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
999         assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
1000                 .that(extractRelativePathForDirectory(directoryPath))
1001                 .isEqualTo(relativePath);
1002     }
1003 
computeDataValues(String path)1004     private static ContentValues computeDataValues(String path) {
1005         final ContentValues values = new ContentValues();
1006         values.put(MediaColumns.DATA, path);
1007         FileUtils.computeValuesFromData(values, /*forFuse*/ false);
1008         Log.v(TAG, "Computed values " + values);
1009         return values;
1010     }
1011 
assertBucket(ContentValues values, String bucketId, String bucketName)1012     private static void assertBucket(ContentValues values, String bucketId, String bucketName) {
1013         if (bucketId != null) {
1014             assertEquals(bucketName,
1015                     values.getAsString(ImageColumns.BUCKET_DISPLAY_NAME));
1016             assertEquals(bucketId.toLowerCase(Locale.ROOT).hashCode(),
1017                     (long) values.getAsLong(ImageColumns.BUCKET_ID));
1018         } else {
1019             assertNull(values.get(ImageColumns.BUCKET_DISPLAY_NAME));
1020             assertNull(values.get(ImageColumns.BUCKET_ID));
1021         }
1022     }
1023 
assertVolume(ContentValues values, String volumeName)1024     private static void assertVolume(ContentValues values, String volumeName) {
1025         assertEquals(volumeName, values.getAsString(ImageColumns.VOLUME_NAME));
1026     }
1027 
assertRelativePath(ContentValues values, String relativePath)1028     private static void assertRelativePath(ContentValues values, String relativePath) {
1029         assertEquals(relativePath, values.get(ImageColumns.RELATIVE_PATH));
1030     }
1031 
assertMimetype(ContentValues values, String type)1032     private static void assertMimetype(ContentValues values, String type) {
1033         assertEquals(type, values.get(MediaColumns.MIME_TYPE));
1034     }
1035 
assertDisplayName(ContentValues values, String type)1036     private static void assertDisplayName(ContentValues values, String type) {
1037         assertEquals(type, values.get(MediaColumns.DISPLAY_NAME));
1038     }
1039 
isGreylistMatch(String raw)1040     private static boolean isGreylistMatch(String raw) {
1041         for (Pattern p : MediaProvider.sGreylist) {
1042             if (p.matcher(raw).matches()) {
1043                 return true;
1044             }
1045         }
1046         return false;
1047     }
1048 
buildFile(Uri uri, String relativePath, String displayName, String mimeType)1049     private String buildFile(Uri uri, String relativePath, String displayName,
1050             String mimeType) {
1051         final ContentValues values = new ContentValues();
1052         if (relativePath != null) {
1053             values.put(MediaColumns.RELATIVE_PATH, relativePath);
1054         }
1055         values.put(MediaColumns.DISPLAY_NAME, displayName);
1056         values.put(MediaColumns.MIME_TYPE, mimeType);
1057         try {
1058             ensureFileColumns(uri, values);
1059         } catch (VolumeArgumentException | VolumeNotFoundException e) {
1060             throw e.rethrowAsIllegalArgumentException();
1061         }
1062         return values.getAsString(MediaColumns.DATA);
1063     }
1064 
ensureFileColumns(Uri uri, ContentValues values)1065     private void ensureFileColumns(Uri uri, ContentValues values)
1066             throws VolumeArgumentException, VolumeNotFoundException {
1067         try (ContentProviderClient cpc = sIsolatedResolver
1068                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
1069             ((MediaProvider) cpc.getLocalContentProvider())
1070                     .ensureFileColumns(uri, values);
1071         }
1072     }
1073 
assertEndsWith(String expected, String actual)1074     private static void assertEndsWith(String expected, String actual) {
1075         if (!actual.endsWith(expected)) {
1076             fail("Expected ends with " + expected + " but found " + actual);
1077         }
1078     }
1079 
assertThrows(Class<T> clazz, Runnable r)1080     private static <T extends Exception> void assertThrows(Class<T> clazz, Runnable r) {
1081         try {
1082             r.run();
1083             fail("Expected " + clazz + " to be thrown");
1084         } catch (Exception e) {
1085             if (!clazz.isAssignableFrom(e.getClass())) {
1086                 throw e;
1087             }
1088         }
1089     }
1090 
1091     @Test
testNestedTransaction_applyBatch()1092     public void testNestedTransaction_applyBatch() throws Exception {
1093         final Uri[] uris = new Uri[] {
1094                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, 0),
1095                 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 0),
1096         };
1097         final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
1098         ops.add(ContentProviderOperation.newDelete(uris[0]).build());
1099         ops.add(ContentProviderOperation.newDelete(uris[1]).build());
1100         sIsolatedResolver.applyBatch(MediaStore.AUTHORITY, ops);
1101     }
1102 }
1103