• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.cts.mediastorageapp;
18 
19 import static android.provider.MediaStore.VOLUME_EXTERNAL;
20 import static android.scopedstorage.cts.lib.TestUtils.doEscalation;
21 
22 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 import static com.google.common.truth.Truth.assertWithMessage;
26 
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.app.RecoverableSecurityException;
35 import android.content.ContentResolver;
36 import android.content.ContentUris;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.net.Uri;
43 import android.os.Environment;
44 import android.os.FileUtils;
45 import android.os.ParcelFileDescriptor;
46 import android.provider.MediaStore;
47 import android.provider.MediaStore.MediaColumns;
48 import android.support.test.uiautomator.UiDevice;
49 import android.support.test.uiautomator.UiObject;
50 import android.support.test.uiautomator.UiObjectNotFoundException;
51 import android.support.test.uiautomator.UiScrollable;
52 import android.support.test.uiautomator.UiSelector;
53 
54 import androidx.test.InstrumentationRegistry;
55 import androidx.test.runner.AndroidJUnit4;
56 
57 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingParams;
58 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingSession;
59 
60 import com.google.common.io.ByteStreams;
61 
62 import org.junit.Before;
63 import org.junit.Test;
64 import org.junit.runner.RunWith;
65 
66 import java.io.File;
67 import java.io.FileInputStream;
68 import java.io.FileNotFoundException;
69 import java.io.FileOutputStream;
70 import java.io.IOException;
71 import java.io.InputStream;
72 import java.io.OutputStream;
73 import java.io.PrintWriter;
74 import java.nio.charset.StandardCharsets;
75 import java.util.Arrays;
76 import java.util.HashSet;
77 import java.util.concurrent.Callable;
78 import java.util.concurrent.TimeoutException;
79 
80 @RunWith(AndroidJUnit4.class)
81 public class MediaStorageTest {
82     private static final File TEST_JPG = Environment.buildPath(
83             Environment.getExternalStorageDirectory(),
84             Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file1.jpg");
85     private static final File TEST_PDF = Environment.buildPath(
86             Environment.getExternalStorageDirectory(),
87             Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file2.pdf");
88 
89     private Context mContext;
90     private ContentResolver mContentResolver;
91     private int mUserId;
92 
93     private static int currentAttempt = 0;
94     private static final int MAX_NUMBER_OF_ATTEMPT = 10;
95 
96     @Before
setUp()97     public void setUp() throws Exception {
98         mContext = InstrumentationRegistry.getTargetContext();
99         mContentResolver = mContext.getContentResolver();
100         mUserId = mContext.getUserId();
101     }
102 
103     @Test
testLegacy()104     public void testLegacy() throws Exception {
105         assertTrue(Environment.isExternalStorageLegacy());
106 
107         // We can always see mounted state
108         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
109 
110         // We might have top-level access
111         final File probe = new File(Environment.getExternalStorageDirectory(),
112                 "cts" + System.nanoTime());
113         assertTrue(probe.createNewFile());
114         assertNotNull(Environment.getExternalStorageDirectory().list());
115 
116         // We always have our package directories
117         final File probePackage = new File(mContext.getExternalFilesDir(null),
118                 "cts" + System.nanoTime());
119         assertTrue(probePackage.createNewFile());
120 
121         assertTrue(TEST_JPG.exists());
122         assertTrue(TEST_PDF.exists());
123 
124         final Uri jpgUri = MediaStore.scanFile(mContentResolver, TEST_JPG);
125         final Uri pdfUri = MediaStore.scanFile(mContentResolver, TEST_PDF);
126 
127         final HashSet<Long> seen = new HashSet<>();
128         try (Cursor c = mContentResolver.query(
129                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
130                 new String[]{MediaColumns._ID}, null, null)) {
131             while (c.moveToNext()) {
132                 seen.add(c.getLong(0));
133             }
134         }
135 
136         assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
137         assertTrue(seen.contains(ContentUris.parseId(pdfUri)));
138     }
139 
140     @Test
testStageFiles()141     public void testStageFiles() throws Exception {
142         final File jpg = stageFile(TEST_JPG);
143         assertTrue(jpg.exists());
144         final File pdf = stageFile(TEST_PDF);
145         assertTrue(pdf.exists());
146     }
147 
148     @Test
testClearFiles()149     public void testClearFiles() throws Exception {
150         TEST_JPG.delete();
151         assertNull(MediaStore.scanFile(mContentResolver, TEST_JPG));
152         TEST_PDF.delete();
153         assertNull(MediaStore.scanFile(mContentResolver, TEST_PDF));
154     }
155 
156     @Test
testMediaNone()157     public void testMediaNone() throws Exception {
158         doMediaNone(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
159         doMediaNone(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
160         doMediaNone(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
161 
162         // But since we don't hold the Music permission, we can't read the
163         // indexed metadata
164         try (Cursor c = mContentResolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
165                 null, null, null)) {
166             assertEquals(0, c.getCount());
167         }
168         try (Cursor c = mContentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
169                 null, null, null)) {
170             assertEquals(0, c.getCount());
171         }
172         try (Cursor c = mContentResolver.query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
173                 null, null, null)) {
174             assertEquals(0, c.getCount());
175         }
176     }
177 
doMediaNone(Uri collection, Callable<Uri> create)178     private void doMediaNone(Uri collection, Callable<Uri> create) throws Exception {
179         final Uri red = create.call();
180         final Uri blue = create.call();
181 
182         clearMediaOwner(blue, mUserId);
183 
184         // Since we have no permissions, we should only be able to see media
185         // that we've contributed
186         final HashSet<Long> seen = new HashSet<>();
187         try (Cursor c = mContentResolver.query(collection,
188                 new String[]{MediaColumns._ID}, null, null)) {
189             while (c.moveToNext()) {
190                 seen.add(c.getLong(0));
191             }
192         }
193 
194         assertTrue(seen.contains(ContentUris.parseId(red)));
195         assertFalse(seen.contains(ContentUris.parseId(blue)));
196 
197         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
198         }
199         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
200             fail("Expected read access to be blocked");
201         } catch (SecurityException | FileNotFoundException expected) {
202         }
203         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
204             fail("Expected write access to be blocked");
205         } catch (SecurityException | FileNotFoundException expected) {
206         }
207 
208         // Verify that we can't grant ourselves access
209         for (int flag : new int[]{
210                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
211                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION
212         }) {
213             try {
214                 mContext.grantUriPermission(mContext.getPackageName(), blue, flag);
215                 fail("Expected granting to be blocked for flag 0x" + Integer.toHexString(flag));
216             } catch (SecurityException expected) {
217             }
218         }
219     }
220 
221     /**
222      * If the app grants read UriPermission to the uri without id (E.g.
223      * MediaStore.Audio.Media.EXTERNAL_CONTENT_URI), the query result of the uri should be the
224      * same without granting permission.
225      */
226     @Test
testReadUriPermissionOnUriWithoutId_sameQueryResult()227     public void testReadUriPermissionOnUriWithoutId_sameQueryResult() throws Exception {
228         // For Audio, Image, Video, If the app doesn't have delete access to the uri,
229         // MediaProvider throws SecurityException to give callers interacting with a specific media
230         // item a chance to escalate access if they don't already have it. Check SecurityException
231         // for them.
232         doReadUriPermissionOnUriWithoutId_sameQueryResult(
233                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
234                 MediaStorageTest::createAudio, /* checkExceptionForDelete= */ true);
235         doReadUriPermissionOnUriWithoutId_sameQueryResult(
236                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
237                 MediaStorageTest::createVideo,/* checkExceptionForDelete= */  true);
238         doReadUriPermissionOnUriWithoutId_sameQueryResult(
239                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
240                 MediaStorageTest::createImage,/* checkExceptionForDelete= */  true);
241 
242         doReadUriPermissionOnUriWithoutId_sameQueryResult(
243                 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
244                 MediaStorageTest::createAudio, /* checkExceptionForDelete= */ true);
245         doReadUriPermissionOnUriWithoutId_sameQueryResult(
246                 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
247                 MediaStorageTest::createAudio, /* checkExceptionForDelete= */ true);
248         doReadUriPermissionOnUriWithoutId_sameQueryResult(
249                 MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
250                 MediaStorageTest::createAudio, /* checkExceptionForDelete= */ true);
251 
252         doReadUriPermissionOnUriWithoutId_sameQueryResult(
253                 MediaStore.Downloads.EXTERNAL_CONTENT_URI,
254                 MediaStorageTest::createDownload,/* checkExceptionForDelete= */  false);
255         doReadUriPermissionOnUriWithoutId_sameQueryResult(
256                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
257                 MediaStorageTest::createFile,/* checkExceptionForDelete= */  false);
258         doReadUriPermissionOnUriWithoutId_sameQueryResult(
259                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
260                 MediaStorageTest::createPlaylist,/* checkExceptionForDelete= */  false);
261     }
262 
263     /**
264      * If the app grants read UriPermission to the uri without id (E.g.
265      * MediaStore.Audio.Media.EXTERNAL_CONTENT_URI), the query result of the uri should be the
266      * same without granting permission.
267      */
doReadUriPermissionOnUriWithoutId_sameQueryResult(Uri collectionUri, Callable<Uri> create, boolean checkExceptionForDelete)268     private void doReadUriPermissionOnUriWithoutId_sameQueryResult(Uri collectionUri,
269             Callable<Uri> create, boolean checkExceptionForDelete) throws Exception {
270         final int flagGrantRead = Intent.FLAG_GRANT_READ_URI_PERMISSION;
271         final Uri red = create.call();
272         final Uri blue = create.call();
273         clearMediaOwner(blue, mUserId);
274         final int originalCount;
275 
276         try {
277             try (Cursor c = mContentResolver.query(collectionUri, new String[]{MediaColumns._ID},
278                     null, null)) {
279                 originalCount = c.getCount();
280             }
281 
282             mContext.grantUriPermission(mContext.getPackageName(), collectionUri, flagGrantRead);
283             try (Cursor c = mContentResolver.query(collectionUri, new String[]{MediaColumns._ID},
284                     null, null)) {
285                 assertWithMessage("After grant read UriPermission to " + collectionUri.toString()
286                         + ", the item count of the query result").that(c.getCount()).isEqualTo(
287                         originalCount);
288             }
289 
290             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
291             }
292 
293             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
294                 fail("Expected read access to " + blue.toString() + " be blocked");
295             } catch (SecurityException | FileNotFoundException expected) {
296             }
297 
298             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
299                 fail("Expected write access to " + blue.toString() + " be blocked");
300             } catch (SecurityException | FileNotFoundException expected) {
301             }
302 
303             // If checkExceptionForDelete is true, throws SecurityException is as we expected.
304             // Otherwise, the app doesn't have delete access to the file, the deleted count is 0.
305             if (checkExceptionForDelete) {
306                 try {
307                     mContentResolver.delete(blue, null);
308                     fail("Expected delete access to " + blue.toString() + " be blocked");
309                 } catch (SecurityException expected) {
310                 }
311             } else {
312                 final int count = mContentResolver.delete(blue, null);
313                 assertThat(count).isEqualTo(0);
314             }
315         } finally {
316             mContext.revokeUriPermission(mContext.getPackageName(), collectionUri, flagGrantRead);
317         }
318     }
319 
320     /**
321      * b/197302116. The apps can't be granted prefix UriPermissions to the uri, when the query
322      * result of the uri is 1.
323      */
324     @Test
testOwningOneFileNotGrantPrefixUriPermission()325     public void testOwningOneFileNotGrantPrefixUriPermission() throws Exception {
326         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
327                 MediaStorageTest::createAudio);
328         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
329                 MediaStorageTest::createVideo);
330         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
331                 MediaStorageTest::createImage);
332         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Downloads.EXTERNAL_CONTENT_URI,
333                 MediaStorageTest::createDownload);
334         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
335                 MediaStorageTest::createFile);
336         doOwningOneFileNotGrantPrefixUriPermission(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
337                 MediaStorageTest::createPlaylist);
338     }
339 
340     /**
341      * The apps can't be granted prefix UriPermissions to the uri without id, when the query result
342      * of the uri is 1.
343      */
doOwningOneFileNotGrantPrefixUriPermission(Uri collectionUri, Callable<Uri> create)344     private void doOwningOneFileNotGrantPrefixUriPermission(Uri collectionUri, Callable<Uri> create)
345             throws Exception {
346 
347         clearOwnFiles(collectionUri);
348 
349         final Uri red = create.call();
350         final Uri blue = create.call();
351         clearMediaOwner(blue, mUserId);
352 
353         try (Cursor c = mContentResolver.query(collectionUri, new String[]{MediaColumns._ID}, null,
354                 null)) {
355             assertThat(c.getCount()).isEqualTo(1);
356             c.moveToFirst();
357             assertThat(c.getLong(0)).isEqualTo(ContentUris.parseId(red));
358         }
359 
360         final int flagGrantReadPrefix =
361                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
362         try {
363             mContext.grantUriPermission(mContext.getPackageName(), collectionUri,
364                     flagGrantReadPrefix);
365             fail("Expected granting to " + collectionUri.toString() + " be blocked for flag 0x"
366                     + Integer.toHexString(flagGrantReadPrefix));
367         } catch (SecurityException expected) {
368         }
369 
370         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "r")) {
371         }
372 
373         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
374             fail("Expected read access to " + blue.toString() + " be blocked");
375         } catch (SecurityException | FileNotFoundException expected) {
376         }
377 
378         final int flagGrantWritePrefix = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
379                 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
380         try {
381             mContext.grantUriPermission(mContext.getPackageName(), collectionUri,
382                     flagGrantWritePrefix);
383             fail("Expected granting to " + collectionUri.toString() + " be blocked for flag 0x"
384                     + Integer.toHexString(flagGrantWritePrefix));
385         } catch (SecurityException expected) {
386         }
387 
388         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
389         }
390 
391         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
392             fail("Expected write access to " + blue.toString() + " be blocked");
393         } catch (SecurityException | FileNotFoundException expected) {
394         }
395     }
396 
397     @Test
testGrantUriPermission()398     public void testGrantUriPermission() throws Exception {
399         doGrantUriPermission_nonPrefixAndPrefix();
400         doGrantUriPermission_prefix();
401     }
402 
403     /**
404      * Test prefix and non-prefix uri grant for all packages
405      */
doGrantUriPermission_nonPrefixAndPrefix()406     private void doGrantUriPermission_nonPrefixAndPrefix() {
407         final int flagGrantRead = Intent.FLAG_GRANT_READ_URI_PERMISSION;
408         final int flagGrantWrite = Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
409         final int flagGrantReadPrefix =
410                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
411         final int flagGrantWritePrefix =
412                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
413 
414         for (Uri uri : new Uri[]{
415                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
416                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
417                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
418                 MediaStore.Downloads.EXTERNAL_CONTENT_URI,
419                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL)
420         }) {
421             // Non-prefix grant
422             checkGrantUriPermission(uri, flagGrantRead, /* isGrantAllowed */ true);
423             checkGrantUriPermission(uri, flagGrantWrite, /* isGrantAllowed */ true);
424 
425             // Prefix grant
426             checkGrantUriPermission(uri, flagGrantReadPrefix, /* isGrantAllowed */ false);
427             checkGrantUriPermission(uri, flagGrantWritePrefix, /* isGrantAllowed */ false);
428 
429             // revoke granted permissions
430             mContext.revokeUriPermission(uri, flagGrantRead | flagGrantWrite);
431         }
432     }
433 
434     /**
435      * b/194539422. Test prefix uri grant for all packages
436      */
doGrantUriPermission_prefix()437     private void doGrantUriPermission_prefix() {
438         final int flagGrantReadPrefix =
439                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
440         final int flagGrantWritePrefix =
441                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
442 
443         for (Uri uri : new Uri[]{
444                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
445                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
446                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
447                 MediaStore.Downloads.EXTERNAL_CONTENT_URI,
448                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL)
449         }) {
450             checkGrantUriPermission(uri, flagGrantReadPrefix, /* isGrantAllowed */ false);
451             checkGrantUriPermission(uri, flagGrantWritePrefix, /* isGrantAllowed */ false);
452         }
453     }
454 
checkGrantUriPermission(Uri uri, int mode, boolean isGrantAllowed)455     private void checkGrantUriPermission(Uri uri, int mode, boolean isGrantAllowed) {
456         if (isGrantAllowed) {
457             mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
458         } else {
459             try {
460                 mContext.grantUriPermission(mContext.getPackageName(), uri, mode);
461                 fail("Expected granting to " + uri.toString() + " be blocked for flag 0x"
462                         + Integer.toHexString(mode));
463             } catch (SecurityException expected) {
464             }
465         }
466     }
467 
468     @Test
testMediaRead()469     public void testMediaRead() throws Exception {
470         doMediaRead(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
471         doMediaRead(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
472         doMediaRead(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
473     }
474 
doMediaRead(Uri collection, Callable<Uri> create)475     private void doMediaRead(Uri collection, Callable<Uri> create) throws Exception {
476         final Uri red = create.call();
477         final Uri blue = create.call();
478 
479         clearMediaOwner(blue, mUserId);
480 
481         // Holding read permission we can see items we don't own
482         final HashSet<Long> seen = new HashSet<>();
483         try (Cursor c = mContentResolver.query(collection,
484                 new String[]{MediaColumns._ID}, null, null)) {
485             while (c.moveToNext()) {
486                 seen.add(c.getLong(0));
487             }
488         }
489 
490         assertTrue(seen.contains(ContentUris.parseId(red)));
491         assertTrue(seen.contains(ContentUris.parseId(blue)));
492 
493         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
494         }
495         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
496         }
497         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
498             fail("Expected write access to be blocked");
499         } catch (SecurityException | FileNotFoundException expected) {
500         }
501     }
502 
503     @Test
testMediaWrite()504     public void testMediaWrite() throws Exception {
505         doMediaWrite(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
506         doMediaWrite(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
507         doMediaWrite(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
508     }
509 
doMediaWrite(Uri collection, Callable<Uri> create)510     private void doMediaWrite(Uri collection, Callable<Uri> create) throws Exception {
511         final Uri red = create.call();
512         final Uri blue = create.call();
513 
514         clearMediaOwner(blue, mUserId);
515 
516         // Holding read permission we can see items we don't own
517         final HashSet<Long> seen = new HashSet<>();
518         try (Cursor c = mContentResolver.query(collection,
519                 new String[]{MediaColumns._ID}, null, null)) {
520             while (c.moveToNext()) {
521                 seen.add(c.getLong(0));
522             }
523         }
524 
525         assertTrue(seen.contains(ContentUris.parseId(red)));
526         assertTrue(seen.contains(ContentUris.parseId(blue)));
527 
528         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
529         }
530         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
531         }
532         if (Environment.isExternalStorageLegacy()) {
533             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
534             }
535         } else {
536             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
537                 fail("Expected write access to be blocked");
538             } catch (SecurityException | FileNotFoundException expected) {
539             }
540         }
541     }
542 
543     @Test
testMediaEscalation_Open()544     public void testMediaEscalation_Open() throws Exception {
545         doMediaEscalation_Open(MediaStorageTest::createAudio);
546         doMediaEscalation_Open(MediaStorageTest::createVideo);
547         doMediaEscalation_Open(MediaStorageTest::createImage);
548     }
549 
doMediaEscalation_Open(Callable<Uri> create)550     private void doMediaEscalation_Open(Callable<Uri> create) throws Exception {
551         final Uri red = create.call();
552         clearMediaOwner(red, mUserId);
553 
554         RecoverableSecurityException exception = null;
555         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
556             fail("Expected write access to be blocked");
557         } catch (RecoverableSecurityException expected) {
558             exception = expected;
559         }
560 
561         doEscalation(exception);
562 
563         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
564         }
565     }
566 
567     @Test
testMediaEscalation_Update()568     public void testMediaEscalation_Update() throws Exception {
569         doMediaEscalation_Update(MediaStorageTest::createAudio);
570         doMediaEscalation_Update(MediaStorageTest::createVideo);
571         doMediaEscalation_Update(MediaStorageTest::createImage);
572     }
573 
doMediaEscalation_Update(Callable<Uri> create)574     private void doMediaEscalation_Update(Callable<Uri> create) throws Exception {
575         final Uri red = create.call();
576         clearMediaOwner(red, mUserId);
577 
578         final ContentValues values = new ContentValues();
579         values.put(MediaColumns.DISPLAY_NAME, "cts" + System.nanoTime());
580 
581         RecoverableSecurityException exception = null;
582         try {
583             mContentResolver.update(red, values, null, null);
584             fail("Expected update access to be blocked");
585         } catch (RecoverableSecurityException expected) {
586             exception = expected;
587         }
588 
589         doEscalation(exception);
590 
591         assertEquals(1, mContentResolver.update(red, values, null, null));
592     }
593 
594     @Test
testMediaEscalation_Delete()595     public void testMediaEscalation_Delete() throws Exception {
596         doMediaEscalation_Delete(MediaStorageTest::createAudio);
597         doMediaEscalation_Delete(MediaStorageTest::createVideo);
598         doMediaEscalation_Delete(MediaStorageTest::createImage);
599     }
600 
doMediaEscalation_Delete(Callable<Uri> create)601     private void doMediaEscalation_Delete(Callable<Uri> create) throws Exception {
602         final Uri red = create.call();
603         clearMediaOwner(red, mUserId);
604 
605         RecoverableSecurityException exception = null;
606         try {
607             mContentResolver.delete(red, null, null);
608             fail("Expected update access to be blocked");
609         } catch (RecoverableSecurityException expected) {
610             exception = expected;
611         }
612 
613         doEscalation(exception);
614 
615         assertEquals(1, mContentResolver.delete(red, null, null));
616     }
617 
618     @Test
testMediaEscalation_RequestWriteFilePathSupport()619     public void testMediaEscalation_RequestWriteFilePathSupport() throws Exception {
620         doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createAudio);
621         doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createVideo);
622         doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createImage);
623         doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createPlaylist);
624         doMediaEscalation_RequestWrite_withFilePathSupport(MediaStorageTest::createSubtitle);
625     }
626 
doMediaEscalation_RequestWrite_withFilePathSupport( Callable<Uri> create)627     private void doMediaEscalation_RequestWrite_withFilePathSupport(
628             Callable<Uri> create) throws Exception {
629         final Uri red = create.call();
630         assertNotNull(red);
631         String path = queryForSingleColumn(red, MediaColumns.DATA);
632         File file = new File(path);
633         assertThat(file.exists()).isTrue();
634         assertThat(file.canRead()).isTrue();
635         assertThat(file.canWrite()).isTrue();
636 
637         clearMediaOwner(red, mUserId);
638         assertThat(file.canWrite()).isFalse();
639 
640         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
641             fail("Expected write access to be blocked");
642         } catch (SecurityException expected) {
643         }
644 
645         doEscalation(MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)));
646 
647         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
648         }
649         // Wait for MediaStore to be idle to avoid flakiness due to race conditions
650         MediaStore.waitForIdle(mContentResolver);
651 
652         // Check File API support
653         assertAccessFileAPISupport(file);
654         assertReadWriteFileAPISupport(file);
655         assertRenameFileAPISupport(file);
656         assertRenameAndReplaceFileAPISupport(file, create);
657         assertDeleteFileAPISupport(file);
658     }
659 
assertAccessFileAPISupport(File file)660     private void assertAccessFileAPISupport(File file) throws Exception {
661         assertThat(file.canRead()).isTrue();
662         assertThat(file.canWrite()).isTrue();
663     }
664 
assertReadWriteFileAPISupport(File file)665     private void assertReadWriteFileAPISupport(File file) throws Exception {
666         final String str = "Just some random text";
667         final byte[] bytes = str.getBytes();
668         // Write to file
669         try (FileOutputStream fos = new FileOutputStream(file)) {
670             fos.write(bytes);
671         }
672         // Read the same data from file
673         try (FileInputStream fis = new FileInputStream(file)) {
674             assertThat(ByteStreams.toByteArray(fis)).isEqualTo(bytes);
675         }
676     }
677 
assertRenameFileAPISupport(File oldFile)678     public void assertRenameFileAPISupport(File oldFile) throws Exception {
679         final String oldName = oldFile.getAbsolutePath();
680         final String extension = oldName.substring(oldName.lastIndexOf('.')).trim();
681         // Rename to same extension so test app does not lose access to file.
682         final String newRelativeName = "cts" + System.nanoTime() + extension;
683         final File newFile = Environment.buildPath(
684                 Environment.getExternalStorageDirectory(),
685                 Environment.DIRECTORY_DOWNLOADS,
686                 newRelativeName);
687         final String newName = newFile.getAbsolutePath();
688         assertWithMessage("Rename from oldName [%s] to newName [%s]", oldName, newName)
689                 .that(oldFile.renameTo(newFile))
690                 .isTrue();
691         // Rename back to oldFile for other ops like delete
692         assertWithMessage("Rename back from newName [%s] to oldName [%s]", newName, oldName)
693                 .that(newFile.renameTo(oldFile))
694                 .isTrue();
695     }
696 
assertRenameAndReplaceFileAPISupport(File oldFile, Callable<Uri> create)697     public void assertRenameAndReplaceFileAPISupport(File oldFile, Callable<Uri> create)
698             throws Exception {
699         final String oldName = oldFile.getAbsolutePath();
700 
701         // Create new file to which we do not have any access.
702         final Uri newUri = create.call();
703         assertWithMessage("Check newFile created").that(newUri).isNotNull();
704         File newFile = new File(queryForSingleColumn(newUri, MediaColumns.DATA));
705         final String newName = newFile.getAbsolutePath();
706         clearMediaOwner(newUri, mUserId);
707 
708         assertWithMessage(
709                 "Rename should fail without newFile grant from oldName [%s] to newName [%s]",
710                 oldName, newName)
711                 .that(oldFile.renameTo(newFile))
712                 .isFalse();
713 
714         // Grant access to newFile and rename should succeed.
715         doEscalation(
716                 MediaStore.createWriteRequest(mContentResolver, Arrays.asList(newUri)));
717         assertWithMessage("Rename from oldName [%s] to newName [%s]", oldName, newName)
718                 .that(oldFile.renameTo(newFile))
719                 .isTrue();
720 
721         // We need to request grant on newUri again, since the rename above caused the URI grant
722         // to be revoked.
723         doEscalation(
724                 MediaStore.createWriteRequest(mContentResolver, Arrays.asList(newUri)));
725         // Rename back to oldFile for other ops like delete
726         assertWithMessage("Rename back from newName [%s] to oldName [%s]", newName, oldName)
727                 .that(newFile.renameTo(oldFile))
728                 .isTrue();
729     }
730 
assertDeleteFileAPISupport(File file)731     private void assertDeleteFileAPISupport(File file) throws Exception {
732         assertThat(file.delete()).isTrue();
733     }
734 
735     @Test
testMediaEscalation_RequestWrite()736     public void testMediaEscalation_RequestWrite() throws Exception {
737         doMediaEscalation_RequestWrite(true /* allowAccess */,
738                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
739     }
740 
741     @Test
testMediaEscalationWithDenied_RequestWrite()742     public void testMediaEscalationWithDenied_RequestWrite() throws Exception {
743         doMediaEscalation_RequestWrite(false /* allowAccess */,
744                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
745     }
746 
747     @Test
testMediaEscalation_RequestWrite_showConfirmDialog()748     public void testMediaEscalation_RequestWrite_showConfirmDialog() throws Exception {
749         doMediaEscalation_RequestWrite(true /* allowAccess */,
750                 true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
751     }
752 
753     @Test
testMediaEscalation_RequestWrite_notShowConfirmDialog()754     public void testMediaEscalation_RequestWrite_notShowConfirmDialog() throws Exception {
755         doMediaEscalation_RequestWrite(true /* allowAccess */,
756                 true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
757     }
758 
doMediaEscalation_RequestWrite(boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)759     private void doMediaEscalation_RequestWrite(boolean allowAccess,
760             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
761         doMediaEscalation_RequestWrite(MediaStorageTest::createAudio, allowAccess,
762                 shouldCheckDialogShownValue, isDialogShownExpected);
763         doMediaEscalation_RequestWrite(MediaStorageTest::createVideo, allowAccess,
764                 shouldCheckDialogShownValue, isDialogShownExpected);
765         doMediaEscalation_RequestWrite(MediaStorageTest::createImage, allowAccess,
766                 shouldCheckDialogShownValue, isDialogShownExpected);
767         doMediaEscalation_RequestWrite(MediaStorageTest::createPlaylist, allowAccess,
768                 shouldCheckDialogShownValue, isDialogShownExpected);
769         doMediaEscalation_RequestWrite(MediaStorageTest::createSubtitle, allowAccess,
770                 shouldCheckDialogShownValue, isDialogShownExpected);
771     }
772 
doMediaEscalation_RequestWrite(Callable<Uri> create, boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)773     private void doMediaEscalation_RequestWrite(Callable<Uri> create, boolean allowAccess,
774             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
775         final Uri red = create.call();
776         clearMediaOwner(red, mUserId);
777 
778         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
779             fail("Expected write access to be blocked");
780         } catch (SecurityException expected) {
781         }
782 
783         if (allowAccess) {
784             doEscalation(
785                     MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
786                     true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
787 
788             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
789             }
790         } else {
791             doEscalation(
792                     MediaStore.createWriteRequest(mContentResolver, Arrays.asList(red)),
793                     false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
794             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
795                 fail("Expected write access to be blocked");
796             } catch (SecurityException expected) {
797             }
798         }
799     }
800 
801     @Test
testMediaEscalationWithDenied_RequestUnTrash()802     public void testMediaEscalationWithDenied_RequestUnTrash() throws Exception {
803         doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createAudio);
804         doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createVideo);
805         doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createImage);
806         doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createPlaylist);
807         doMediaEscalationWithDenied_RequestUnTrash(MediaStorageTest::createSubtitle);
808     }
809 
doMediaEscalationWithDenied_RequestUnTrash(Callable<Uri> create)810     private void doMediaEscalationWithDenied_RequestUnTrash(Callable<Uri> create) throws Exception {
811         final Uri red = create.call();
812         clearMediaOwner(red, mUserId);
813 
814         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
815         doEscalation(
816                 MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true));
817         assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
818         doEscalation(
819                 MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
820                 false /* allowAccess */, false /* shouldCheckDialogShownValue */,
821                 false /* isDialogShownExpected */);
822         assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
823     }
824 
825     @Test
testMediaEscalation_RequestTrash()826     public void testMediaEscalation_RequestTrash() throws Exception {
827         doMediaEscalation_RequestTrash(true /* allowAccess */,
828                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
829     }
830 
831     @Test
testMediaEscalationWithDenied_RequestTrash()832     public void testMediaEscalationWithDenied_RequestTrash() throws Exception {
833         doMediaEscalation_RequestTrash(false /* allowAccess */,
834                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
835     }
836 
837     @Test
testMediaEscalation_RequestTrash_showConfirmDialog()838     public void testMediaEscalation_RequestTrash_showConfirmDialog() throws Exception {
839         doMediaEscalation_RequestTrash(true /* allowAccess */,
840                 true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
841     }
842 
843     @Test
testMediaEscalation_RequestTrash_notShowConfirmDialog()844     public void testMediaEscalation_RequestTrash_notShowConfirmDialog() throws Exception {
845         doMediaEscalation_RequestTrash(true /* allowAccess */,
846                 true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
847     }
848 
doMediaEscalation_RequestTrash(boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)849     private void doMediaEscalation_RequestTrash(boolean allowAccess,
850             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
851         doMediaEscalation_RequestTrash(MediaStorageTest::createAudio, allowAccess,
852                 shouldCheckDialogShownValue, isDialogShownExpected);
853         doMediaEscalation_RequestTrash(MediaStorageTest::createVideo, allowAccess,
854                 shouldCheckDialogShownValue, isDialogShownExpected);
855         doMediaEscalation_RequestTrash(MediaStorageTest::createImage, allowAccess,
856                 shouldCheckDialogShownValue, isDialogShownExpected);
857         doMediaEscalation_RequestTrash(MediaStorageTest::createPlaylist, allowAccess,
858                 shouldCheckDialogShownValue, isDialogShownExpected);
859         doMediaEscalation_RequestTrash(MediaStorageTest::createSubtitle, allowAccess,
860                 shouldCheckDialogShownValue, isDialogShownExpected);
861     }
862 
doMediaEscalation_RequestTrash(Callable<Uri> create, boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)863     private void doMediaEscalation_RequestTrash(Callable<Uri> create, boolean allowAccess,
864             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
865         final Uri red = create.call();
866         clearMediaOwner(red, mUserId);
867 
868         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
869 
870         if (allowAccess) {
871             doEscalation(
872                     MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
873                     true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
874             assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
875             doEscalation(
876                     MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), false),
877                     true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
878             assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
879         } else {
880             doEscalation(
881                     MediaStore.createTrashRequest(mContentResolver, Arrays.asList(red), true),
882                     false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
883             assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_TRASHED));
884         }
885     }
886 
887     @Test
testMediaEscalation_RequestFavorite()888     public void testMediaEscalation_RequestFavorite() throws Exception {
889         doMediaEscalation_RequestFavorite(MediaStorageTest::createAudio);
890         doMediaEscalation_RequestFavorite(MediaStorageTest::createVideo);
891         doMediaEscalation_RequestFavorite(MediaStorageTest::createImage);
892         doMediaEscalation_RequestFavorite(MediaStorageTest::createPlaylist);
893         doMediaEscalation_RequestFavorite(MediaStorageTest::createSubtitle);
894     }
895 
doMediaEscalation_RequestFavorite(Callable<Uri> create)896     private void doMediaEscalation_RequestFavorite(Callable<Uri> create) throws Exception {
897         final Uri red = create.call();
898         clearMediaOwner(red, mUserId);
899 
900         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
901         doEscalation(
902                 MediaStore.createFavoriteRequest(mContentResolver, Arrays.asList(red), true));
903         assertEquals("1", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
904         doEscalation(
905                 MediaStore.createFavoriteRequest(mContentResolver, Arrays.asList(red), false));
906         assertEquals("0", queryForSingleColumn(red, MediaColumns.IS_FAVORITE));
907     }
908 
909     @Test
testMediaEscalation_RequestDelete()910     public void testMediaEscalation_RequestDelete() throws Exception {
911         doMediaEscalation_RequestDelete(true /* allowAccess */,
912                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
913     }
914 
915     @Test
testMediaEscalationWithDenied_RequestDelete()916     public void testMediaEscalationWithDenied_RequestDelete() throws Exception {
917         doMediaEscalation_RequestDelete(false /* allowAccess */,
918                 false /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
919     }
920 
921     @Test
testMediaEscalation_RequestDelete_showConfirmDialog()922     public void testMediaEscalation_RequestDelete_showConfirmDialog() throws Exception {
923         doMediaEscalation_RequestDelete(true /* allowAccess */,
924                 true /* shouldCheckDialogShownValue */, true /* isDialogShownExpected */);
925     }
926 
927     @Test
testMediaEscalation_RequestDelete_notShowConfirmDialog()928     public void testMediaEscalation_RequestDelete_notShowConfirmDialog() throws Exception {
929         doMediaEscalation_RequestDelete(true /* allowAccess */,
930                 true /* shouldCheckDialogShownValue */, false /* isDialogShownExpected */);
931     }
932 
doMediaEscalation_RequestDelete(boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)933     private void doMediaEscalation_RequestDelete(boolean allowAccess,
934             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
935         doMediaEscalation_RequestDelete(MediaStorageTest::createAudio, allowAccess,
936                 shouldCheckDialogShownValue, isDialogShownExpected);
937         doMediaEscalation_RequestDelete(MediaStorageTest::createVideo, allowAccess,
938                 shouldCheckDialogShownValue, isDialogShownExpected);
939         doMediaEscalation_RequestDelete(MediaStorageTest::createImage, allowAccess,
940                 shouldCheckDialogShownValue, isDialogShownExpected);
941         doMediaEscalation_RequestDelete(MediaStorageTest::createPlaylist, allowAccess,
942                 shouldCheckDialogShownValue, isDialogShownExpected);
943         doMediaEscalation_RequestDelete(MediaStorageTest::createSubtitle, allowAccess,
944                 shouldCheckDialogShownValue, isDialogShownExpected);
945     }
946 
doMediaEscalation_RequestDelete(Callable<Uri> create, boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)947     private void doMediaEscalation_RequestDelete(Callable<Uri> create, boolean allowAccess,
948             boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception {
949         final Uri red = create.call();
950         clearMediaOwner(red, mUserId);
951 
952         try (Cursor c = mContentResolver.query(red, null, null, null)) {
953             assertEquals(1, c.getCount());
954         }
955 
956         if (allowAccess) {
957             doEscalation(
958                     MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
959                     true /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
960             try (Cursor c = mContentResolver.query(red, null, null, null)) {
961                 assertEquals(0, c.getCount());
962             }
963         } else {
964             doEscalation(
965                     MediaStore.createDeleteRequest(mContentResolver, Arrays.asList(red)),
966                     false /* allowAccess */, shouldCheckDialogShownValue, isDialogShownExpected);
967             try (Cursor c = mContentResolver.query(red, null, null, null)) {
968                 assertEquals(1, c.getCount());
969             }
970         }
971     }
972 
scrollIntoView(UiSelector selector)973     private static void scrollIntoView(UiSelector selector) {
974         UiScrollable uiScrollable = new UiScrollable(new UiSelector().scrollable(true));
975         try {
976             uiScrollable.scrollIntoView(selector);
977         } catch (UiObjectNotFoundException e) {
978             // Scrolling can fail if the UI is not scrollable
979         }
980     }
981 
createDownload()982     private static Uri createDownload() throws IOException {
983         final String content = "<html><body>Content</body></html>";
984         final String displayName = "cts" + System.nanoTime();
985         final String mimeType = "text/html";
986         final Context context = InstrumentationRegistry.getTargetContext();
987         final PendingParams params = new PendingParams(
988                 MediaStore.Downloads.EXTERNAL_CONTENT_URI, displayName, mimeType);
989 
990         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
991         assertNotNull(pendingUri);
992         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
993             try (PrintWriter pw = new PrintWriter(session.openOutputStream())) {
994                 pw.print(content);
995             }
996             try (OutputStream out = session.openOutputStream()) {
997                 out.write(content.getBytes(StandardCharsets.UTF_8));
998             }
999             return session.publish();
1000         }
1001     }
1002 
createFile()1003     private static Uri createFile() throws IOException {
1004         return createSubtitle();
1005     }
1006 
createAudio()1007     private static Uri createAudio() throws IOException {
1008         final Context context = InstrumentationRegistry.getTargetContext();
1009         final String displayName = "cts" + System.nanoTime();
1010         final PendingParams params = new PendingParams(
1011                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, displayName, "audio/mpeg");
1012         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
1013 
1014         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
1015             try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
1016                  OutputStream out = session.openOutputStream()) {
1017                 FileUtils.copy(in, out);
1018             }
1019             return session.publish();
1020         }
1021     }
1022 
createVideo()1023     private static Uri createVideo() throws IOException {
1024         final Context context = InstrumentationRegistry.getTargetContext();
1025         final String displayName = "cts" + System.nanoTime();
1026         final PendingParams params = new PendingParams(
1027                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, displayName, "video/mpeg");
1028         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
1029         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
1030             try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
1031                  OutputStream out = session.openOutputStream()) {
1032                 FileUtils.copy(in, out);
1033             }
1034             return session.publish();
1035         }
1036     }
1037 
createImage()1038     private static Uri createImage() throws IOException {
1039         final Context context = InstrumentationRegistry.getTargetContext();
1040         final String displayName = "cts" + System.nanoTime();
1041         final PendingParams params = new PendingParams(
1042                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, displayName, "image/png");
1043         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
1044         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
1045             try (OutputStream out = session.openOutputStream()) {
1046                 final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
1047                 bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
1048             }
1049             return session.publish();
1050         }
1051     }
1052 
createPlaylist()1053     private static Uri createPlaylist() throws IOException {
1054         final Context context = InstrumentationRegistry.getTargetContext();
1055         final String displayName = "cts" + System.nanoTime();
1056         final PendingParams params = new PendingParams(
1057                 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, displayName, "audio/mpegurl");
1058         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
1059         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
1060             return session.publish();
1061         }
1062     }
1063 
createSubtitle()1064     private static Uri createSubtitle() throws IOException {
1065         final Context context = InstrumentationRegistry.getTargetContext();
1066         final String displayName = "cts" + System.nanoTime();
1067         final PendingParams params = new PendingParams(
1068                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL), displayName,
1069                 "application/x-subrip");
1070         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
1071         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
1072             try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
1073                  OutputStream out = session.openOutputStream()) {
1074                 FileUtils.copy(in, out);
1075             }
1076             return session.publish();
1077         }
1078     }
1079 
queryForSingleColumn(Uri uri, String column)1080     private static String queryForSingleColumn(Uri uri, String column) throws Exception {
1081         final ContentResolver resolver = InstrumentationRegistry.getTargetContext()
1082                 .getContentResolver();
1083         try (Cursor c = resolver.query(uri, new String[]{column}, null, null)) {
1084             assertEquals(1, c.getCount());
1085             assertTrue(c.moveToFirst());
1086             return c.getString(0);
1087         }
1088     }
1089 
clearOwnFiles(Uri uri)1090     private static void clearOwnFiles(Uri uri) throws Exception {
1091         final ContentResolver resolver = InstrumentationRegistry.getTargetContext()
1092                 .getContentResolver();
1093         try (Cursor c = resolver.query(uri, new String[]{MediaColumns._ID}, null, null)) {
1094             while (c.moveToNext()) {
1095                 final long id = c.getLong(0);
1096                 final Uri contentUri = ContentUris.withAppendedId(uri, id);
1097                 resolver.delete(contentUri, null);
1098             }
1099         }
1100     }
1101 
clearMediaOwner(Uri uri, int userId)1102     private static void clearMediaOwner(Uri uri, int userId) throws IOException {
1103         final String cmd = String.format(
1104                 "content update --uri %s --user %d --bind owner_package_name:n:",
1105                 uri, userId);
1106         runShellCommand(InstrumentationRegistry.getInstrumentation(), cmd);
1107     }
1108 
stageFile(File file)1109     static File stageFile(File file) throws Exception {
1110         // Sometimes file creation fails due to slow permission update, try more times
1111         while (currentAttempt < MAX_NUMBER_OF_ATTEMPT) {
1112             try {
1113                 file.getParentFile().mkdirs();
1114                 file.createNewFile();
1115                 return file;
1116             } catch (IOException e) {
1117                 currentAttempt++;
1118                 // wait 500ms
1119                 Thread.sleep(500);
1120             }
1121         }
1122         throw new TimeoutException("File creation failed due to slow permission update");
1123     }
1124 
1125 }
1126