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