• 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 com.android.compatibility.common.util.SystemUtil.runShellCommand;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assert.fail;
27 
28 import android.app.Activity;
29 import android.app.Instrumentation;
30 import android.app.RecoverableSecurityException;
31 import android.content.ContentResolver;
32 import android.content.ContentUris;
33 import android.content.ContentValues;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.database.Cursor;
37 import android.graphics.Bitmap;
38 import android.net.Uri;
39 import android.os.Environment;
40 import android.os.FileUtils;
41 import android.os.ParcelFileDescriptor;
42 import android.provider.MediaStore;
43 import android.provider.MediaStore.MediaColumns;
44 import android.support.test.uiautomator.UiDevice;
45 import android.support.test.uiautomator.UiSelector;
46 
47 import androidx.test.InstrumentationRegistry;
48 import androidx.test.runner.AndroidJUnit4;
49 
50 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingParams;
51 import com.android.cts.mediastorageapp.MediaStoreUtils.PendingSession;
52 
53 import org.junit.Before;
54 import org.junit.Test;
55 import org.junit.runner.RunWith;
56 
57 import java.io.File;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.OutputStream;
62 import java.util.HashSet;
63 import java.util.concurrent.Callable;
64 
65 @RunWith(AndroidJUnit4.class)
66 public class MediaStorageTest {
67     private static final File TEST_JPG = Environment.buildPath(
68             Environment.getExternalStorageDirectory(),
69             Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file1.jpg");
70     private static final File TEST_PDF = Environment.buildPath(
71             Environment.getExternalStorageDirectory(),
72             Environment.DIRECTORY_DOWNLOADS, "mediastoragetest_file2.pdf");
73 
74     private Context mContext;
75     private ContentResolver mContentResolver;
76     private int mUserId;
77 
78     @Before
setUp()79     public void setUp() throws Exception {
80         mContext = InstrumentationRegistry.getTargetContext();
81         mContentResolver = mContext.getContentResolver();
82         mUserId = mContext.getUserId();
83     }
84 
85     @Test
testSandboxed()86     public void testSandboxed() throws Exception {
87         doSandboxed(true);
88     }
89 
90     @Test
testNotSandboxed()91     public void testNotSandboxed() throws Exception {
92         doSandboxed(false);
93     }
94 
95     @Test
testStageFiles()96     public void testStageFiles() throws Exception {
97         final File jpg = stageFile(TEST_JPG);
98         assertTrue(jpg.exists());
99         final File pdf = stageFile(TEST_PDF);
100         assertTrue(pdf.exists());
101     }
102 
103     @Test
testClearFiles()104     public void testClearFiles() throws Exception {
105         TEST_JPG.delete();
106         assertNull(MediaStore.scanFileFromShell(mContext, TEST_JPG));
107         TEST_PDF.delete();
108         assertNull(MediaStore.scanFileFromShell(mContext, TEST_PDF));
109     }
110 
doSandboxed(boolean sandboxed)111     private void doSandboxed(boolean sandboxed) throws Exception {
112         assertEquals(!sandboxed, Environment.isExternalStorageLegacy());
113 
114         // We can always see mounted state
115         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
116 
117         // We might have top-level access
118         final File probe = new File(Environment.getExternalStorageDirectory(),
119                 "cts" + System.nanoTime());
120         if (sandboxed) {
121             try {
122                 probe.createNewFile();
123                 fail();
124             } catch (IOException expected) {
125             }
126             assertNull(Environment.getExternalStorageDirectory().list());
127         } else {
128             assertTrue(probe.createNewFile());
129             assertNotNull(Environment.getExternalStorageDirectory().list());
130         }
131 
132         // We always have our package directories
133         final File probePackage = new File(mContext.getExternalFilesDir(null),
134                 "cts" + System.nanoTime());
135         assertTrue(probePackage.createNewFile());
136 
137         assertTrue(TEST_JPG.exists());
138         assertTrue(TEST_PDF.exists());
139 
140         final Uri jpgUri = MediaStore.scanFileFromShell(mContext, TEST_JPG);
141         final Uri pdfUri = MediaStore.scanFileFromShell(mContext, TEST_PDF);
142 
143         final HashSet<Long> seen = new HashSet<>();
144         try (Cursor c = mContentResolver.query(
145                 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
146                 new String[] { MediaColumns._ID }, null, null)) {
147             while (c.moveToNext()) {
148                 seen.add(c.getLong(0));
149             }
150         }
151 
152         if (sandboxed) {
153             // If we're sandboxed, we should only see the image
154             assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
155             assertFalse(seen.contains(ContentUris.parseId(pdfUri)));
156         } else {
157             // If we're not sandboxed, we should see both
158             assertTrue(seen.contains(ContentUris.parseId(jpgUri)));
159             assertTrue(seen.contains(ContentUris.parseId(pdfUri)));
160         }
161     }
162 
163     @Test
testMediaNone()164     public void testMediaNone() throws Exception {
165         doMediaNone(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
166         doMediaNone(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
167         doMediaNone(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
168 
169         // But since we don't hold the Music permission, we can't read the
170         // indexed metadata
171         try (Cursor c = mContentResolver.query(MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
172                 null, null, null)) {
173             assertEquals(0, c.getCount());
174         }
175         try (Cursor c = mContentResolver.query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
176                 null, null, null)) {
177             assertEquals(0, c.getCount());
178         }
179         try (Cursor c = mContentResolver.query(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
180                 null, null, null)) {
181             assertEquals(0, c.getCount());
182         }
183     }
184 
doMediaNone(Uri collection, Callable<Uri> create)185     private void doMediaNone(Uri collection, Callable<Uri> create) throws Exception {
186         final Uri red = create.call();
187         final Uri blue = create.call();
188 
189         clearMediaOwner(blue, mUserId);
190 
191         // Since we have no permissions, we should only be able to see media
192         // that we've contributed
193         final HashSet<Long> seen = new HashSet<>();
194         try (Cursor c = mContentResolver.query(collection,
195                 new String[] { MediaColumns._ID }, null, null)) {
196             while (c.moveToNext()) {
197                 seen.add(c.getLong(0));
198             }
199         }
200 
201         assertTrue(seen.contains(ContentUris.parseId(red)));
202         assertFalse(seen.contains(ContentUris.parseId(blue)));
203 
204         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
205         }
206         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
207             fail("Expected read access to be blocked");
208         } catch (SecurityException | FileNotFoundException expected) {
209         }
210         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
211             fail("Expected write access to be blocked");
212         } catch (SecurityException | FileNotFoundException expected) {
213         }
214     }
215 
216     @Test
testMediaRead()217     public void testMediaRead() throws Exception {
218         doMediaRead(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
219         doMediaRead(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
220         doMediaRead(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
221     }
222 
doMediaRead(Uri collection, Callable<Uri> create)223     private void doMediaRead(Uri collection, Callable<Uri> create) throws Exception {
224         final Uri red = create.call();
225         final Uri blue = create.call();
226 
227         clearMediaOwner(blue, mUserId);
228 
229         // Holding read permission we can see items we don't own
230         final HashSet<Long> seen = new HashSet<>();
231         try (Cursor c = mContentResolver.query(collection,
232                 new String[] { MediaColumns._ID }, null, null)) {
233             while (c.moveToNext()) {
234                 seen.add(c.getLong(0));
235             }
236         }
237 
238         assertTrue(seen.contains(ContentUris.parseId(red)));
239         assertTrue(seen.contains(ContentUris.parseId(blue)));
240 
241         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
242         }
243         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
244         }
245         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
246             fail("Expected write access to be blocked");
247         } catch (SecurityException | FileNotFoundException expected) {
248         }
249     }
250 
251     @Test
testMediaWrite()252     public void testMediaWrite() throws Exception {
253         doMediaWrite(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createAudio);
254         doMediaWrite(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createVideo);
255         doMediaWrite(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStorageTest::createImage);
256     }
257 
doMediaWrite(Uri collection, Callable<Uri> create)258     private void doMediaWrite(Uri collection, Callable<Uri> create) throws Exception {
259         final Uri red = create.call();
260         final Uri blue = create.call();
261 
262         clearMediaOwner(blue, mUserId);
263 
264         // Holding read permission we can see items we don't own
265         final HashSet<Long> seen = new HashSet<>();
266         try (Cursor c = mContentResolver.query(collection,
267                 new String[] { MediaColumns._ID }, null, null)) {
268             while (c.moveToNext()) {
269                 seen.add(c.getLong(0));
270             }
271         }
272 
273         assertTrue(seen.contains(ContentUris.parseId(red)));
274         assertTrue(seen.contains(ContentUris.parseId(blue)));
275 
276         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "rw")) {
277         }
278         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "r")) {
279         }
280         if (Environment.isExternalStorageLegacy()) {
281             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
282             }
283         } else {
284             try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(blue, "w")) {
285                 fail("Expected write access to be blocked");
286             } catch (SecurityException | FileNotFoundException expected) {
287             }
288         }
289     }
290 
291     @Test
testMediaEscalation_Open()292     public void testMediaEscalation_Open() throws Exception {
293         doMediaEscalation_Open(MediaStorageTest::createAudio);
294         doMediaEscalation_Open(MediaStorageTest::createVideo);
295         doMediaEscalation_Open(MediaStorageTest::createImage);
296     }
297 
doMediaEscalation_Open(Callable<Uri> create)298     private void doMediaEscalation_Open(Callable<Uri> create) throws Exception {
299         final Uri red = create.call();
300         clearMediaOwner(red, mUserId);
301 
302         RecoverableSecurityException exception = null;
303         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
304             fail("Expected write access to be blocked");
305         } catch (RecoverableSecurityException expected) {
306             exception = expected;
307         }
308 
309         doEscalation(exception);
310 
311         try (ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(red, "w")) {
312         }
313     }
314 
315     @Test
testMediaEscalation_Update()316     public void testMediaEscalation_Update() throws Exception {
317         doMediaEscalation_Update(MediaStorageTest::createAudio);
318         doMediaEscalation_Update(MediaStorageTest::createVideo);
319         doMediaEscalation_Update(MediaStorageTest::createImage);
320     }
321 
doMediaEscalation_Update(Callable<Uri> create)322     private void doMediaEscalation_Update(Callable<Uri> create) throws Exception {
323         final Uri red = create.call();
324         clearMediaOwner(red, mUserId);
325 
326         final ContentValues values = new ContentValues();
327         values.put(MediaColumns.DISPLAY_NAME, "cts" + System.nanoTime());
328 
329         RecoverableSecurityException exception = null;
330         try {
331             mContentResolver.update(red, values, null, null);
332             fail("Expected update access to be blocked");
333         } catch (RecoverableSecurityException expected) {
334             exception = expected;
335         }
336 
337         doEscalation(exception);
338 
339         assertEquals(1, mContentResolver.update(red, values, null, null));
340     }
341 
342     @Test
testMediaEscalation_Delete()343     public void testMediaEscalation_Delete() throws Exception {
344         doMediaEscalation_Delete(MediaStorageTest::createAudio);
345         doMediaEscalation_Delete(MediaStorageTest::createVideo);
346         doMediaEscalation_Delete(MediaStorageTest::createImage);
347     }
348 
doMediaEscalation_Delete(Callable<Uri> create)349     private void doMediaEscalation_Delete(Callable<Uri> create) throws Exception {
350         final Uri red = create.call();
351         clearMediaOwner(red, mUserId);
352 
353         RecoverableSecurityException exception = null;
354         try {
355             mContentResolver.delete(red, null, null);
356             fail("Expected update access to be blocked");
357         } catch (RecoverableSecurityException expected) {
358             exception = expected;
359         }
360 
361         doEscalation(exception);
362 
363         assertEquals(1, mContentResolver.delete(red, null, null));
364     }
365 
doEscalation(RecoverableSecurityException exception)366     private void doEscalation(RecoverableSecurityException exception) throws Exception {
367         // Try launching the action to grant ourselves access
368         final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
369         final Intent intent = new Intent(inst.getContext(), GetResultActivity.class);
370         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
371 
372         // Wake up the device and dismiss the keyguard before the test starts
373         final UiDevice device = UiDevice.getInstance(inst);
374         device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
375         device.executeShellCommand("wm dismiss-keyguard");
376 
377         final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent);
378         device.waitForIdle();
379         activity.clearResult();
380         activity.startIntentSenderForResult(
381                 exception.getUserAction().getActionIntent().getIntentSender(),
382                 42, null, 0, 0, 0);
383 
384         device.waitForIdle();
385         device.findObject(new UiSelector().textMatches("(?i:Allow)")).click();
386 
387         // Verify that we now have access
388         final GetResultActivity.Result res = activity.getResult();
389         assertEquals(Activity.RESULT_OK, res.resultCode);
390     }
391 
createAudio()392     private static Uri createAudio() throws IOException {
393         final Context context = InstrumentationRegistry.getTargetContext();
394         final String displayName = "cts" + System.nanoTime();
395         final PendingParams params = new PendingParams(
396                 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, displayName, "audio/mpeg");
397         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
398         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
399             try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
400                     OutputStream out = session.openOutputStream()) {
401                 FileUtils.copy(in, out);
402             }
403             return session.publish();
404         }
405     }
406 
createVideo()407     private static Uri createVideo() throws IOException {
408         final Context context = InstrumentationRegistry.getTargetContext();
409         final String displayName = "cts" + System.nanoTime();
410         final PendingParams params = new PendingParams(
411                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, displayName, "video/mpeg");
412         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
413         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
414             try (InputStream in = context.getResources().getAssets().open("testmp3.mp3");
415                     OutputStream out = session.openOutputStream()) {
416                 FileUtils.copy(in, out);
417             }
418             return session.publish();
419         }
420     }
421 
createImage()422     private static Uri createImage() throws IOException {
423         final Context context = InstrumentationRegistry.getTargetContext();
424         final String displayName = "cts" + System.nanoTime();
425         final PendingParams params = new PendingParams(
426                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, displayName, "image/png");
427         final Uri pendingUri = MediaStoreUtils.createPending(context, params);
428         try (PendingSession session = MediaStoreUtils.openPending(context, pendingUri)) {
429             try (OutputStream out = session.openOutputStream()) {
430                 final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
431                 bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
432             }
433             return session.publish();
434         }
435     }
436 
clearMediaOwner(Uri uri, int userId)437     private static void clearMediaOwner(Uri uri, int userId) throws IOException {
438         final String cmd = String.format(
439                 "content update --uri %s --user %d --bind owner_package_name:n:",
440                 uri, userId);
441         runShellCommand(InstrumentationRegistry.getInstrumentation(), cmd);
442     }
443 
stageFile(File file)444     static File stageFile(File file) throws IOException {
445         file.getParentFile().mkdirs();
446         file.createNewFile();
447         return file;
448     }
449 }
450