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