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