1 /** 2 * Copyright (C) 2020 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 android.scopedstorage.cts.lib; 18 19 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY; 20 21 import static androidx.test.InstrumentationRegistry.getContext; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static junit.framework.Assert.assertEquals; 27 import static junit.framework.TestCase.assertNotNull; 28 29 import static org.junit.Assert.assertNotEquals; 30 import static org.junit.Assert.fail; 31 32 import android.Manifest; 33 import android.app.ActivityManager; 34 import android.app.AppOpsManager; 35 import android.app.UiAutomation; 36 import android.content.BroadcastReceiver; 37 import android.content.ContentResolver; 38 import android.content.ContentUris; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.IntentFilter; 43 import android.content.pm.PackageManager; 44 import android.database.Cursor; 45 import android.net.Uri; 46 import android.os.Bundle; 47 import android.os.Environment; 48 import android.os.ParcelFileDescriptor; 49 import android.os.storage.StorageManager; 50 import android.provider.MediaStore; 51 import android.system.ErrnoException; 52 import android.system.Os; 53 import android.system.OsConstants; 54 import android.text.TextUtils; 55 import android.util.Log; 56 57 import androidx.annotation.NonNull; 58 import androidx.annotation.Nullable; 59 import androidx.core.os.BuildCompat; 60 import androidx.test.InstrumentationRegistry; 61 62 import com.android.cts.install.lib.Install; 63 import com.android.cts.install.lib.InstallUtils; 64 import com.android.cts.install.lib.TestApp; 65 import com.android.cts.install.lib.Uninstall; 66 67 import com.google.common.io.ByteStreams; 68 69 import java.io.File; 70 import java.io.FileDescriptor; 71 import java.io.FileInputStream; 72 import java.io.IOException; 73 import java.io.InputStream; 74 import java.io.InterruptedIOException; 75 import java.util.ArrayList; 76 import java.util.Arrays; 77 import java.util.HashMap; 78 import java.util.List; 79 import java.util.Locale; 80 import java.util.Optional; 81 import java.util.concurrent.CountDownLatch; 82 import java.util.concurrent.TimeUnit; 83 import java.util.concurrent.TimeoutException; 84 import java.util.function.Supplier; 85 86 /** 87 * General helper functions for ScopedStorageTest tests. 88 */ 89 public class TestUtils { 90 static final String TAG = "ScopedStorageTest"; 91 92 public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType"; 93 public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path"; 94 public static final String INTENT_EXTRA_URI = "android.scopedstorage.cts.uri"; 95 public static final String INTENT_EXTRA_CALLING_PKG = "android.scopedstorage.cts.calling_pkg"; 96 public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception"; 97 public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile"; 98 public static final String CREATE_IMAGE_ENTRY_QUERY = 99 "android.scopedstorage.cts.createimageentry"; 100 public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile"; 101 public static final String CAN_OPEN_FILE_FOR_READ_QUERY = 102 "android.scopedstorage.cts.can_openfile_read"; 103 public static final String CAN_OPEN_FILE_FOR_WRITE_QUERY = 104 "android.scopedstorage.cts.can_openfile_write"; 105 public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ = 106 "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_read"; 107 public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE = 108 "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_write"; 109 public static final String IS_URI_REDACTED_VIA_FILEPATH = 110 "android.scopedstorage.cts.is_uri_redacted_via_filepath"; 111 public static final String QUERY_URI = "android.scopedstorage.cts.query_uri"; 112 public static final String OPEN_FILE_FOR_READ_QUERY = 113 "android.scopedstorage.cts.openfile_read"; 114 public static final String OPEN_FILE_FOR_WRITE_QUERY = 115 "android.scopedstorage.cts.openfile_write"; 116 public static final String CAN_READ_WRITE_QUERY = 117 "android.scopedstorage.cts.can_read_and_write"; 118 public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir"; 119 public static final String SETATTR_QUERY = "android.scopedstorage.cts.setattr"; 120 public static final String CHECK_DATABASE_ROW_EXISTS_QUERY = 121 "android.scopedstorage.cts.check_database_row_exists"; 122 public static final String RENAME_FILE_QUERY = "android.scopedstorage.cts.renamefile"; 123 124 public static final String STR_DATA1 = "Just some random text"; 125 public static final String STR_DATA2 = "More arbitrary stuff"; 126 127 public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes(); 128 public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes(); 129 130 public static final String RENAME_FILE_PARAMS_SEPARATOR = ";"; 131 132 // Root of external storage 133 private static File sExternalStorageDirectory = Environment.getExternalStorageDirectory(); 134 private static String sStorageVolumeName = MediaStore.VOLUME_EXTERNAL; 135 136 /** 137 * Set this to {@code false} if the test is verifying uri grants on testApp. Force stopping the 138 * app will kill the app and it will lose uri grants. 139 */ 140 private static boolean sShouldForceStopTestApp = true; 141 142 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); 143 private static final long POLLING_SLEEP_MILLIS = 100; 144 145 /** 146 * Creates the top level default directories. 147 * 148 * <p>Those are usually created by MediaProvider, but some naughty tests might delete them 149 * and not restore them afterwards, so we make sure we create them before we make any 150 * assumptions about their existence. 151 */ setupDefaultDirectories()152 public static void setupDefaultDirectories() { 153 for (File dir : getDefaultTopLevelDirs()) { 154 dir.mkdirs(); 155 assertWithMessage("Could not setup default dir [%s]", dir.toString()) 156 .that(dir.exists()) 157 .isTrue(); 158 } 159 } 160 161 /** 162 * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package. 163 */ grantPermission(String packageName, String permission)164 public static void grantPermission(String packageName, String permission) { 165 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 166 uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS"); 167 try { 168 uiAutomation.grantRuntimePermission(packageName, permission); 169 } finally { 170 uiAutomation.dropShellPermissionIdentity(); 171 } 172 try { 173 pollForPermission(packageName, permission, true); 174 } catch (Exception e) { 175 fail("Exception on polling for permission grant for " + packageName + " for " 176 + permission + ": " + e.getMessage()); 177 } 178 } 179 180 /** 181 * Revokes permissions from the given package. 182 */ revokePermission(String packageName, String permission)183 public static void revokePermission(String packageName, String permission) { 184 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 185 uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS"); 186 try { 187 uiAutomation.revokeRuntimePermission(packageName, permission); 188 } finally { 189 uiAutomation.dropShellPermissionIdentity(); 190 } 191 try { 192 pollForPermission(packageName, permission, false); 193 } catch (Exception e) { 194 fail("Exception on polling for permission revoke for " + packageName + " for " 195 + permission + ": " + e.getMessage()); 196 } 197 } 198 199 /** 200 * Adopts shell permission identity for the given permissions. 201 */ adoptShellPermissionIdentity(String... permissions)202 public static void adoptShellPermissionIdentity(String... permissions) { 203 InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( 204 permissions); 205 } 206 207 /** 208 * Drops shell permission identity for all permissions. 209 */ dropShellPermissionIdentity()210 public static void dropShellPermissionIdentity() { 211 InstrumentationRegistry.getInstrumentation().getUiAutomation() 212 .dropShellPermissionIdentity(); 213 } 214 215 /** 216 * Executes a shell command. 217 */ executeShellCommand(String pattern, Object...args)218 public static String executeShellCommand(String pattern, Object...args) throws IOException { 219 String command = String.format(pattern, args); 220 int attempt = 0; 221 while (attempt++ < 5) { 222 try { 223 return executeShellCommandInternal(command); 224 } catch (InterruptedIOException e) { 225 // Hmm, we had trouble executing the shell command; the best we 226 // can do is try again a few more times 227 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 228 } 229 } 230 throw new IOException("Failed to execute " + command); 231 } 232 executeShellCommandInternal(String cmd)233 private static String executeShellCommandInternal(String cmd) throws IOException { 234 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 235 try (FileInputStream output = new FileInputStream( 236 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 237 return new String(ByteStreams.toByteArray(output)); 238 } 239 } 240 241 /** 242 * Makes the given {@code testApp} list the content of the given directory and returns the 243 * result as an {@link ArrayList} 244 */ listAs(TestApp testApp, String dirPath)245 public static ArrayList<String> listAs(TestApp testApp, String dirPath) throws Exception { 246 return getContentsFromTestApp(testApp, dirPath, READDIR_QUERY); 247 } 248 249 /** 250 * Returns {@code true} iff the given {@code path} exists and is readable and 251 * writable for for {@code testApp}. 252 */ canReadAndWriteAs(TestApp testApp, String path)253 public static boolean canReadAndWriteAs(TestApp testApp, String path) throws Exception { 254 return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY); 255 } 256 257 /** 258 * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the 259 * result as an {@link HashMap} 260 */ readExifMetadataFromTestApp( TestApp testApp, String filePath)261 public static HashMap<String, String> readExifMetadataFromTestApp( 262 TestApp testApp, String filePath) throws Exception { 263 HashMap<String, String> res = 264 getMetadataFromTestApp(testApp, filePath, EXIF_METADATA_QUERY); 265 return res; 266 } 267 268 /** 269 * Makes the given {@code testApp} create a file. 270 * 271 * <p>This method drops shell permission identity. 272 */ createFileAs(TestApp testApp, String path)273 public static boolean createFileAs(TestApp testApp, String path) throws Exception { 274 return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY); 275 } 276 277 /** 278 * Makes the given {@code testApp} create a mediastore DB entry under 279 * {@code MediaStore.Media.Images}. 280 * 281 * The {@code path} argument is treated as a relative path and a name separated 282 * by an {@code '/'}. 283 */ createImageEntryAs(TestApp testApp, String path)284 public static boolean createImageEntryAs(TestApp testApp, String path) throws Exception { 285 return getResultFromTestApp(testApp, path, CREATE_IMAGE_ENTRY_QUERY); 286 } 287 288 /** 289 * Makes the given {@code testApp} delete a file. 290 * 291 * <p>This method drops shell permission identity. 292 */ deleteFileAs(TestApp testApp, String path)293 public static boolean deleteFileAs(TestApp testApp, String path) throws Exception { 294 return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY); 295 } 296 297 /** 298 * Makes the given {@code testApp} delete a file. Doesn't throw in case of failure. 299 */ deleteFileAsNoThrow(TestApp testApp, String path)300 public static boolean deleteFileAsNoThrow(TestApp testApp, String path) { 301 try { 302 return deleteFileAs(testApp, path); 303 } catch (Exception e) { 304 Log.e(TAG, 305 "Error occurred while deleting file: " + path + " on behalf of app: " + testApp, 306 e); 307 return false; 308 } 309 } 310 311 /** 312 * Makes the given {@code testApp} open {@code file} for read or write. 313 * 314 * <p>This method drops shell permission identity. 315 */ canOpenFileAs(TestApp testApp, File file, boolean forWrite)316 public static boolean canOpenFileAs(TestApp testApp, File file, boolean forWrite) 317 throws Exception { 318 String actionName = forWrite ? CAN_OPEN_FILE_FOR_WRITE_QUERY : CAN_OPEN_FILE_FOR_READ_QUERY; 319 return getResultFromTestApp(testApp, file.getPath(), actionName); 320 } 321 322 /** 323 * Makes the given {@code testApp} rename give {@code src} to {@code dst}. 324 * 325 * The method concatenates source and destination paths while sending the request to 326 * {@code testApp}. Hence, {@link TestUtils#RENAME_FILE_PARAMS_SEPARATOR} shouldn't be used 327 * in path names. 328 * 329 * <p>This method drops shell permission identity. 330 */ renameFileAs(TestApp testApp, File src, File dst)331 public static boolean renameFileAs(TestApp testApp, File src, File dst) throws Exception { 332 final String paths = String.format("%s%s%s", 333 src.getAbsolutePath(), RENAME_FILE_PARAMS_SEPARATOR, dst.getAbsolutePath()); 334 return getResultFromTestApp(testApp, paths, RENAME_FILE_QUERY); 335 } 336 337 /** 338 * Makes the given {@code testApp} check if a database row exists for given {@code file} 339 * 340 * <p>This method drops shell permission identity. 341 */ checkDatabaseRowExistsAs(TestApp testApp, File file)342 public static boolean checkDatabaseRowExistsAs(TestApp testApp, File file) throws Exception { 343 return getResultFromTestApp(testApp, file.getPath(), CHECK_DATABASE_ROW_EXISTS_QUERY); 344 } 345 346 /** 347 * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd 348 * redacts EXIF metadata. 349 * 350 * <p> This method drops shell permission identity. 351 */ isFileDescriptorRedacted(TestApp testApp, Uri uri)352 public static boolean isFileDescriptorRedacted(TestApp testApp, Uri uri) 353 throws Exception { 354 String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ; 355 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 356 } 357 358 /** 359 * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd 360 * redacts EXIF metadata. 361 * 362 * <p> This method drops shell permission identity. 363 */ canOpenRedactedUriForWrite(TestApp testApp, Uri uri)364 public static boolean canOpenRedactedUriForWrite(TestApp testApp, Uri uri) 365 throws Exception { 366 String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE; 367 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 368 } 369 370 371 /** 372 * Makes the given {@code testApp} open file path associated with {@code uri} and verifies that 373 * the path redacts EXIF metadata. 374 * 375 * <p>This method drops shell permission identity. 376 */ isFileOpenRedacted(TestApp testApp, Uri uri)377 public static boolean isFileOpenRedacted(TestApp testApp, Uri uri) 378 throws Exception { 379 final String actionName = IS_URI_REDACTED_VIA_FILEPATH; 380 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 381 } 382 383 /** 384 * Makes the given {@code testApp} query on {@code uri}. 385 * 386 * <p>This method drops shell permission identity. 387 */ canQueryOnUri(TestApp testApp, Uri uri)388 public static boolean canQueryOnUri(TestApp testApp, Uri uri) throws Exception { 389 final String actionName = QUERY_URI; 390 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 391 } 392 insertFileFromExternalMedia(boolean useRelative)393 public static Uri insertFileFromExternalMedia(boolean useRelative) throws IOException { 394 ContentValues values = new ContentValues(); 395 String filePath = 396 getAndroidMediaDir().toString() + "/" + getContext().getPackageName() + "/" 397 + System.currentTimeMillis(); 398 if (useRelative) { 399 values.put(MediaStore.MediaColumns.RELATIVE_PATH, 400 "Android/media/" + getContext().getPackageName()); 401 values.put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis()); 402 } else { 403 values.put(MediaStore.MediaColumns.DATA, filePath); 404 } 405 406 return getContentResolver().insert( 407 MediaStore.Files.getContentUri(sStorageVolumeName), values); 408 } 409 insertFile(ContentValues values)410 public static void insertFile(ContentValues values) { 411 assertNotNull(getContentResolver().insert( 412 MediaStore.Files.getContentUri(sStorageVolumeName), values)); 413 } 414 updateFile(Uri uri, ContentValues values)415 public static int updateFile(Uri uri, ContentValues values) { 416 return getContentResolver().update(uri, values, new Bundle()); 417 } 418 verifyInsertFromExternalPrivateDirViaRelativePath_denied()419 public static void verifyInsertFromExternalPrivateDirViaRelativePath_denied() throws Exception { 420 resetDefaultExternalStorageVolume(); 421 422 // Test that inserting files from Android/obb/.. is not allowed. 423 final String androidObbDir = getContext().getObbDir().toString(); 424 ContentValues values = new ContentValues(); 425 values.put( 426 MediaStore.MediaColumns.RELATIVE_PATH, 427 androidObbDir.substring(androidObbDir.indexOf("Android"))); 428 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 429 430 // Test that inserting files from Android/data/.. is not allowed. 431 final String androidDataDir = getExternalFilesDir().toString(); 432 values.put( 433 MediaStore.MediaColumns.RELATIVE_PATH, 434 androidDataDir.substring(androidDataDir.indexOf("Android"))); 435 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 436 } 437 verifyInsertFromExternalMediaDirViaRelativePath_allowed()438 public static void verifyInsertFromExternalMediaDirViaRelativePath_allowed() throws Exception { 439 resetDefaultExternalStorageVolume(); 440 441 // Test that inserting files from Android/media/.. is allowed. 442 final String androidMediaDir = getExternalMediaDir().toString(); 443 final ContentValues values = new ContentValues(); 444 values.put( 445 MediaStore.MediaColumns.RELATIVE_PATH, 446 androidMediaDir.substring(androidMediaDir.indexOf("Android"))); 447 insertFile(values); 448 } 449 verifyInsertFromExternalPrivateDirViaData_denied()450 public static void verifyInsertFromExternalPrivateDirViaData_denied() throws Exception { 451 resetDefaultExternalStorageVolume(); 452 453 ContentValues values = new ContentValues(); 454 455 // Test that inserting files from Android/obb/.. is not allowed. 456 final String androidObbDir = 457 getContext().getObbDir().toString() + "/" + System.currentTimeMillis(); 458 values.put(MediaStore.MediaColumns.DATA, androidObbDir); 459 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 460 461 // Test that inserting files from Android/data/.. is not allowed. 462 final String androidDataDir = getExternalFilesDir().toString(); 463 values.put(MediaStore.MediaColumns.DATA, androidDataDir); 464 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 465 } 466 verifyInsertFromExternalMediaDirViaData_allowed()467 public static void verifyInsertFromExternalMediaDirViaData_allowed() throws Exception { 468 resetDefaultExternalStorageVolume(); 469 470 // Test that inserting files from Android/media/.. is allowed. 471 ContentValues values = new ContentValues(); 472 final String androidMediaDirFile = 473 getExternalMediaDir().toString() + "/" + System.currentTimeMillis(); 474 values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile); 475 insertFile(values); 476 } 477 478 // NOTE: While updating, DATA field should be ignored for all the apps including file manager. verifyUpdateToExternalDirsViaData_denied()479 public static void verifyUpdateToExternalDirsViaData_denied() throws Exception { 480 resetDefaultExternalStorageVolume(); 481 Uri uri = insertFileFromExternalMedia(false); 482 483 final String androidMediaDirFile = 484 getExternalMediaDir().toString() + "/" + System.currentTimeMillis(); 485 ContentValues values = new ContentValues(); 486 values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile); 487 assertEquals(0, updateFile(uri, values)); 488 489 final String androidObbDir = 490 getContext().getObbDir().toString() + "/" + System.currentTimeMillis(); 491 values.put(MediaStore.MediaColumns.DATA, androidObbDir); 492 assertEquals(0, updateFile(uri, values)); 493 494 final String androidDataDir = getExternalFilesDir().toString(); 495 values.put(MediaStore.MediaColumns.DATA, androidDataDir); 496 assertEquals(0, updateFile(uri, values)); 497 } 498 verifyUpdateToExternalMediaDirViaRelativePath_allowed()499 public static void verifyUpdateToExternalMediaDirViaRelativePath_allowed() 500 throws IOException { 501 resetDefaultExternalStorageVolume(); 502 Uri uri = insertFileFromExternalMedia(true); 503 504 // Test that update to files from Android/media/.. is allowed. 505 final String androidMediaDir = getExternalMediaDir().toString(); 506 ContentValues values = new ContentValues(); 507 values.put( 508 MediaStore.MediaColumns.RELATIVE_PATH, 509 androidMediaDir.substring(androidMediaDir.indexOf("Android"))); 510 assertNotEquals(0, updateFile(uri, values)); 511 } 512 verifyUpdateToExternalPrivateDirsViaRelativePath_denied()513 public static void verifyUpdateToExternalPrivateDirsViaRelativePath_denied() 514 throws Exception { 515 resetDefaultExternalStorageVolume(); 516 Uri uri = insertFileFromExternalMedia(true); 517 518 // Test that update to files from Android/obb/.. is not allowed. 519 final String androidObbDir = getContext().getObbDir().toString(); 520 ContentValues values = new ContentValues(); 521 values.put( 522 MediaStore.MediaColumns.RELATIVE_PATH, 523 androidObbDir.substring(androidObbDir.indexOf("Android"))); 524 assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values)); 525 526 // Test that update to files from Android/data/.. is not allowed. 527 final String androidDataDir = getExternalFilesDir().toString(); 528 values.put( 529 MediaStore.MediaColumns.RELATIVE_PATH, 530 androidDataDir.substring(androidDataDir.indexOf("Android"))); 531 assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values)); 532 } 533 534 /** 535 * Makes the given {@code testApp} open a file for read or write. 536 * 537 * <p>This method drops shell permission identity. 538 */ openFileAs(TestApp testApp, File file, boolean forWrite)539 public static ParcelFileDescriptor openFileAs(TestApp testApp, File file, boolean forWrite) 540 throws Exception { 541 String actionName = forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY; 542 String mode = forWrite ? "rw" : "r"; 543 return getPfdFromTestApp(testApp, file, actionName, mode); 544 } 545 546 /** 547 * Makes the given {@code testApp} setattr for given file path. 548 * 549 * <p>This method drops shell permission identity. 550 */ setAttrAs(TestApp testApp, String path)551 public static boolean setAttrAs(TestApp testApp, String path) 552 throws Exception { 553 return getResultFromTestApp(testApp, path, SETATTR_QUERY); 554 } 555 556 /** 557 * Installs a {@link TestApp} without storage permissions. 558 */ installApp(TestApp testApp)559 public static void installApp(TestApp testApp) throws Exception { 560 installApp(testApp, /* grantStoragePermission */ false); 561 } 562 563 /** 564 * Installs a {@link TestApp} with storage permissions. 565 */ installAppWithStoragePermissions(TestApp testApp)566 public static void installAppWithStoragePermissions(TestApp testApp) throws Exception { 567 installApp(testApp, /* grantStoragePermission */ true); 568 } 569 570 /** 571 * Installs a {@link TestApp} and may grant it storage permissions. 572 */ installApp(TestApp testApp, boolean grantStoragePermission)573 public static void installApp(TestApp testApp, boolean grantStoragePermission) 574 throws Exception { 575 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 576 try { 577 final String packageName = testApp.getPackageName(); 578 uiAutomation.adoptShellPermissionIdentity( 579 Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES); 580 if (isAppInstalled(testApp)) { 581 Uninstall.packages(packageName); 582 } 583 Install.single(testApp).commit(); 584 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1); 585 if (grantStoragePermission) { 586 grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE); 587 } 588 } finally { 589 uiAutomation.dropShellPermissionIdentity(); 590 } 591 } 592 isAppInstalled(TestApp testApp)593 public static boolean isAppInstalled(TestApp testApp) { 594 return InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1; 595 } 596 597 /** 598 * Uninstalls a {@link TestApp}. 599 */ uninstallApp(TestApp testApp)600 public static void uninstallApp(TestApp testApp) throws Exception { 601 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 602 try { 603 final String packageName = testApp.getPackageName(); 604 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES); 605 606 Uninstall.packages(packageName); 607 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1); 608 } finally { 609 uiAutomation.dropShellPermissionIdentity(); 610 } 611 } 612 613 /** 614 * Uninstalls a {@link TestApp}. Doesn't throw in case of failure. 615 */ uninstallAppNoThrow(TestApp testApp)616 public static void uninstallAppNoThrow(TestApp testApp) { 617 try { 618 uninstallApp(testApp); 619 } catch (Exception e) { 620 Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e); 621 } 622 } 623 getContentResolver()624 public static ContentResolver getContentResolver() { 625 return getContext().getContentResolver(); 626 } 627 628 /** 629 * Inserts a file into the database using {@link MediaStore.MediaColumns#DATA}. 630 */ insertFileUsingDataColumn(@onNull File file)631 public static Uri insertFileUsingDataColumn(@NonNull File file) { 632 final ContentValues values = new ContentValues(); 633 values.put(MediaStore.MediaColumns.DATA, file.getPath()); 634 return getContentResolver().insert(MediaStore.Files.getContentUri(sStorageVolumeName), 635 values); 636 } 637 638 /** 639 * Returns the content URI for images based on the current storage volume. 640 */ getImageContentUri()641 public static Uri getImageContentUri() { 642 return MediaStore.Images.Media.getContentUri(sStorageVolumeName); 643 } 644 645 /** 646 * Renames the given file using {@link ContentResolver} and {@link MediaStore} and APIs. 647 * This method uses the data column, and not all apps can use it. 648 * @see MediaStore.MediaColumns#DATA 649 */ renameWithMediaProvider(@onNull File oldPath, @NonNull File newPath)650 public static int renameWithMediaProvider(@NonNull File oldPath, @NonNull File newPath) { 651 ContentValues values = new ContentValues(); 652 values.put(MediaStore.MediaColumns.DATA, newPath.getPath()); 653 return getContentResolver().update(MediaStore.Files.getContentUri(sStorageVolumeName), 654 values, /*where*/ MediaStore.MediaColumns.DATA + "=?", 655 /*whereArgs*/ new String[] {oldPath.getPath()}); 656 } 657 658 /** 659 * Queries {@link ContentResolver} for a file and returns the corresponding {@link Uri} for its 660 * entry in the database. Returns {@code null} if file doesn't exist in the database. 661 */ 662 @Nullable getFileUri(@onNull File file)663 public static Uri getFileUri(@NonNull File file) { 664 final Uri contentUri = MediaStore.Files.getContentUri(sStorageVolumeName); 665 final int id = getFileRowIdFromDatabase(file); 666 return id == -1 ? null : ContentUris.withAppendedId(contentUri, id); 667 } 668 669 /** 670 * Queries {@link ContentResolver} for a file and returns the corresponding row ID for its 671 * entry in the database. Returns {@code -1} if file is not found. 672 */ getFileRowIdFromDatabase(@onNull File file)673 public static int getFileRowIdFromDatabase(@NonNull File file) { 674 return getFileRowIdFromDatabase(getContentResolver(), file); 675 } 676 677 /** 678 * Queries given {@link ContentResolver} for a file and returns the corresponding row ID for 679 * its entry in the database. Returns {@code -1} if file is not found. 680 */ getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file)681 public static int getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file) { 682 int id = -1; 683 try (Cursor c = queryFile(cr, file, MediaStore.MediaColumns._ID)) { 684 if (c.moveToFirst()) { 685 id = c.getInt(0); 686 } 687 } 688 return id; 689 } 690 691 /** 692 * Queries {@link ContentResolver} for a file and returns the corresponding owner package name 693 * for its entry in the database. 694 */ 695 @Nullable getFileOwnerPackageFromDatabase(@onNull File file)696 public static String getFileOwnerPackageFromDatabase(@NonNull File file) { 697 String ownerPackage = null; 698 try (Cursor c = queryFile(file, MediaStore.MediaColumns.OWNER_PACKAGE_NAME)) { 699 if (c.moveToFirst()) { 700 ownerPackage = c.getString(0); 701 } 702 } 703 return ownerPackage; 704 } 705 706 /** 707 * Queries {@link ContentResolver} for a file and returns the corresponding file size for its 708 * entry in the database. Returns {@code -1} if file is not found. 709 */ 710 @Nullable getFileSizeFromDatabase(@onNull File file)711 public static int getFileSizeFromDatabase(@NonNull File file) { 712 int size = -1; 713 try (Cursor c = queryFile(file, MediaStore.MediaColumns.SIZE)) { 714 if (c.moveToFirst()) { 715 size = c.getInt(0); 716 } 717 } 718 return size; 719 } 720 721 /** 722 * Queries {@link ContentResolver} for a video file and returns a {@link Cursor} with the given 723 * columns. 724 */ 725 @NonNull queryVideoFile(File file, String... projection)726 public static Cursor queryVideoFile(File file, String... projection) { 727 return queryFile(getContentResolver(), 728 MediaStore.Video.Media.getContentUri(sStorageVolumeName), file, 729 /*includePending*/ true, projection); 730 } 731 732 /** 733 * Queries {@link ContentResolver} for an image file and returns a {@link Cursor} with the given 734 * columns. 735 */ 736 @NonNull queryImageFile(File file, String... projection)737 public static Cursor queryImageFile(File file, String... projection) { 738 return queryFile(getContentResolver(), 739 MediaStore.Images.Media.getContentUri(sStorageVolumeName), file, 740 /*includePending*/ true, projection); 741 } 742 743 /** 744 * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its 745 * entry in the database. 746 */ 747 @NonNull getFileMimeTypeFromDatabase(@onNull File file)748 public static String getFileMimeTypeFromDatabase(@NonNull File file) { 749 String mimeType = ""; 750 try (Cursor c = queryFile(file, MediaStore.MediaColumns.MIME_TYPE)) { 751 if (c.moveToFirst()) { 752 mimeType = c.getString(0); 753 } 754 } 755 return mimeType; 756 } 757 758 /** 759 * Sets {@link AppOpsManager#MODE_ALLOWED} for the given {@code ops} and the given {@code uid}. 760 * 761 * <p>This method drops shell permission identity. 762 */ allowAppOpsToUid(int uid, @NonNull String... ops)763 public static void allowAppOpsToUid(int uid, @NonNull String... ops) { 764 setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, ops); 765 } 766 767 /** 768 * Sets {@link AppOpsManager#MODE_ERRORED} for the given {@code ops} and the given {@code uid}. 769 * 770 * <p>This method drops shell permission identity. 771 */ denyAppOpsToUid(int uid, @NonNull String... ops)772 public static void denyAppOpsToUid(int uid, @NonNull String... ops) { 773 setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, ops); 774 } 775 776 /** 777 * Deletes the given file through {@link ContentResolver} and {@link MediaStore} APIs, 778 * and asserts that the file was successfully deleted from the database. 779 */ deleteWithMediaProvider(@onNull File file)780 public static void deleteWithMediaProvider(@NonNull File file) { 781 Bundle extras = new Bundle(); 782 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 783 MediaStore.MediaColumns.DATA + " = ?"); 784 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 785 new String[] {file.getPath()}); 786 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 787 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 788 assertThat(getContentResolver().delete( 789 MediaStore.Files.getContentUri(sStorageVolumeName), extras)).isEqualTo(1); 790 } 791 792 /** 793 * Deletes db rows and files corresponding to uri through {@link ContentResolver} and 794 * {@link MediaStore} APIs. 795 */ deleteWithMediaProviderNoThrow(Uri... uris)796 public static void deleteWithMediaProviderNoThrow(Uri... uris) { 797 for (Uri uri : uris) { 798 if (uri == null) continue; 799 800 try { 801 getContentResolver().delete(uri, Bundle.EMPTY); 802 } catch (Exception ignored) { 803 } 804 } 805 } 806 807 /** 808 * Renames the given file through {@link ContentResolver} and {@link MediaStore} APIs, 809 * and asserts that the file was updated in the database. 810 */ updateDisplayNameWithMediaProvider(Uri uri, String relativePath, String oldDisplayName, String newDisplayName)811 public static void updateDisplayNameWithMediaProvider(Uri uri, String relativePath, 812 String oldDisplayName, String newDisplayName) { 813 String selection = MediaStore.MediaColumns.RELATIVE_PATH + " = ? AND " 814 + MediaStore.MediaColumns.DISPLAY_NAME + " = ?"; 815 String[] selectionArgs = {relativePath + '/', oldDisplayName}; 816 Bundle extras = new Bundle(); 817 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection); 818 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs); 819 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 820 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 821 822 ContentValues values = new ContentValues(); 823 values.put(MediaStore.MediaColumns.DISPLAY_NAME, newDisplayName); 824 825 assertThat(getContentResolver().update(uri, values, extras)).isEqualTo(1); 826 } 827 828 /** 829 * Opens the given file through {@link ContentResolver} and {@link MediaStore} APIs. 830 */ 831 @NonNull openWithMediaProvider(@onNull File file, String mode)832 public static ParcelFileDescriptor openWithMediaProvider(@NonNull File file, String mode) 833 throws Exception { 834 final Uri fileUri = getFileUri(file); 835 assertThat(fileUri).isNotNull(); 836 Log.i(TAG, "Uri: " + fileUri + ". Data: " + file.getPath()); 837 ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(fileUri, mode); 838 assertThat(pfd).isNotNull(); 839 return pfd; 840 } 841 842 /** 843 * Opens the given file via file path 844 */ 845 @NonNull openWithFilePath(File file, boolean forWrite)846 public static ParcelFileDescriptor openWithFilePath(File file, boolean forWrite) 847 throws IOException { 848 return ParcelFileDescriptor.open(file, 849 forWrite 850 ? ParcelFileDescriptor.MODE_READ_WRITE : ParcelFileDescriptor.MODE_READ_ONLY); 851 } 852 853 /** 854 * Returns whether we can open the file. 855 */ canOpen(File file, boolean forWrite)856 public static boolean canOpen(File file, boolean forWrite) { 857 try { 858 openWithFilePath(file, forWrite); 859 return true; 860 } catch (IOException expected) { 861 return false; 862 } 863 } 864 865 /** 866 * Asserts the given operation throws an exception of type {@code T}. 867 */ assertThrows(Class<T> clazz, Operation<Exception> r)868 public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r) 869 throws Exception { 870 assertThrows(clazz, "", r); 871 } 872 873 /** 874 * Asserts the given operation throws an exception of type {@code T}. 875 */ assertThrows( Class<T> clazz, String errMsg, Operation<Exception> r)876 public static <T extends Exception> void assertThrows( 877 Class<T> clazz, String errMsg, Operation<Exception> r) throws Exception { 878 try { 879 r.run(); 880 fail("Expected " + clazz + " to be thrown"); 881 } catch (Exception e) { 882 if (!clazz.isAssignableFrom(e.getClass()) || !e.getMessage().contains(errMsg)) { 883 Log.e(TAG, "Expected " + clazz + " exception with error message: " + errMsg, e); 884 throw e; 885 } 886 } 887 } 888 setShouldForceStopTestApp(boolean value)889 public static void setShouldForceStopTestApp(boolean value) { 890 sShouldForceStopTestApp = value; 891 } 892 893 /** 894 * A functional interface representing an operation that takes no arguments, 895 * returns no arguments and might throw an {@link Exception} of any kind. 896 * 897 * @param T the subclass of {@link java.lang.Exception} that this operation might throw. 898 */ 899 @FunctionalInterface 900 public interface Operation<T extends Exception> { 901 /** 902 * This is the method that gets called for any object that implements this interface. 903 */ run()904 void run() throws T; 905 } 906 907 /** 908 * Deletes the given file. If the file is a directory, then deletes all of its children (files 909 * or directories) recursively. 910 */ deleteRecursively(@onNull File path)911 public static boolean deleteRecursively(@NonNull File path) { 912 if (path.isDirectory()) { 913 for (File child : path.listFiles()) { 914 if (!deleteRecursively(child)) { 915 return false; 916 } 917 } 918 } 919 return path.delete(); 920 } 921 922 /** 923 * Asserts can rename file. 924 */ assertCanRenameFile(File oldFile, File newFile)925 public static void assertCanRenameFile(File oldFile, File newFile) { 926 assertCanRenameFile(oldFile, newFile, /* checkDB */ true); 927 } 928 929 /** 930 * Asserts can rename file and optionally checks if the database is updated after rename. 931 */ assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase)932 public static void assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase) { 933 assertThat(oldFile.renameTo(newFile)).isTrue(); 934 assertThat(oldFile.exists()).isFalse(); 935 assertThat(newFile.exists()).isTrue(); 936 if (checkDatabase) { 937 assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(-1); 938 assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1); 939 } 940 } 941 942 /** 943 * Asserts cannot rename file. 944 */ assertCantRenameFile(File oldFile, File newFile)945 public static void assertCantRenameFile(File oldFile, File newFile) { 946 final int rowId = getFileRowIdFromDatabase(oldFile); 947 assertThat(oldFile.renameTo(newFile)).isFalse(); 948 assertThat(oldFile.exists()).isTrue(); 949 assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(rowId); 950 } 951 952 /** 953 * Asserts can rename directory. 954 */ assertCanRenameDirectory(File oldDirectory, File newDirectory, @Nullable File[] oldFilesList, @Nullable File[] newFilesList)955 public static void assertCanRenameDirectory(File oldDirectory, File newDirectory, 956 @Nullable File[] oldFilesList, @Nullable File[] newFilesList) { 957 assertThat(oldDirectory.renameTo(newDirectory)).isTrue(); 958 assertThat(oldDirectory.exists()).isFalse(); 959 assertThat(newDirectory.exists()).isTrue(); 960 for (File file : oldFilesList != null ? oldFilesList : new File[0]) { 961 assertThat(file.exists()).isFalse(); 962 assertThat(getFileRowIdFromDatabase(file)).isEqualTo(-1); 963 } 964 for (File file : newFilesList != null ? newFilesList : new File[0]) { 965 assertThat(file.exists()).isTrue(); 966 assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1); 967 } 968 } 969 970 /** 971 * Asserts cannot rename directory. 972 */ assertCantRenameDirectory( File oldDirectory, File newDirectory, @Nullable File[] oldFilesList)973 public static void assertCantRenameDirectory( 974 File oldDirectory, File newDirectory, @Nullable File[] oldFilesList) { 975 assertThat(oldDirectory.renameTo(newDirectory)).isFalse(); 976 assertThat(oldDirectory.exists()).isTrue(); 977 for (File file : oldFilesList != null ? oldFilesList : new File[0]) { 978 assertThat(file.exists()).isTrue(); 979 assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1); 980 } 981 } 982 assertMountMode(String packageName, int uid, int expectedMountMode)983 public static void assertMountMode(String packageName, int uid, int expectedMountMode) { 984 adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE"); 985 try { 986 final StorageManager storageManager = getContext().getSystemService( 987 StorageManager.class); 988 final int actualMountMode = storageManager.getExternalStorageMountMode(uid, 989 packageName); 990 assertWithMessage("mount mode (%s=%s, %s=%s) for package %s and uid %s", 991 expectedMountMode, mountModeToString(expectedMountMode), 992 actualMountMode, mountModeToString(actualMountMode), 993 packageName, uid).that(actualMountMode).isEqualTo(expectedMountMode); 994 } finally { 995 dropShellPermissionIdentity(); 996 } 997 } 998 mountModeToString(int mountMode)999 public static String mountModeToString(int mountMode) { 1000 switch (mountMode) { 1001 case 0: 1002 return "EXTERNAL_NONE"; 1003 case 1: 1004 return "DEFAULT"; 1005 case 2: 1006 return "INSTALLER"; 1007 case 3: 1008 return "PASS_THROUGH"; 1009 case 4: 1010 return "ANDROID_WRITABLE"; 1011 default: 1012 return "INVALID(" + mountMode + ")"; 1013 } 1014 } 1015 assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1016 public static void assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, 1017 TestApp testApp, String callingPackage, String fileName) throws Exception { 1018 File[] dataDirs = getContext().getExternalFilesDirs(null); 1019 canReadWriteFilesInDirs(dataDirs, canAccess, testApp, callingPackage, fileName); 1020 } 1021 assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1022 public static void assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, 1023 TestApp testApp, String callingPackage, String fileName) throws Exception { 1024 File[] obbDirs = getContext().getObbDirs(); 1025 canReadWriteFilesInDirs(obbDirs, canAccess, testApp, callingPackage, fileName); 1026 } 1027 canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, String callingPackage, String fileName)1028 private static void canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, 1029 String callingPackage, String fileName) throws Exception { 1030 for (File dir : dirs) { 1031 final File otherAppExternalDataDir = new File(dir.getPath().replace( 1032 callingPackage, testApp.getPackageName())); 1033 final File file = new File(otherAppExternalDataDir, fileName); 1034 try { 1035 assertThat(file.exists()).isFalse(); 1036 1037 assertThat(createFileAs(testApp, file.getPath())).isTrue(); 1038 if (canAccess) { 1039 assertThat(file.canRead()).isTrue(); 1040 assertThat(file.canWrite()).isTrue(); 1041 } else { 1042 assertThat(file.canRead()).isFalse(); 1043 assertThat(file.canWrite()).isFalse(); 1044 } 1045 } finally { 1046 deleteFileAsNoThrow(testApp, file.getAbsolutePath()); 1047 } 1048 } 1049 } 1050 1051 /** 1052 * Polls for external storage to be mounted. 1053 */ pollForExternalStorageState()1054 public static void pollForExternalStorageState() throws Exception { 1055 pollForCondition( 1056 () -> Environment.getExternalStorageState(getExternalStorageDir()) 1057 .equals(Environment.MEDIA_MOUNTED), 1058 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 1059 } 1060 1061 /** 1062 * Polls until we're granted or denied a given permission. 1063 */ pollForPermission(String perm, boolean granted)1064 public static void pollForPermission(String perm, boolean granted) throws Exception { 1065 pollForCondition(() -> granted == checkPermissionAndAppOp(perm), 1066 "Timed out while waiting for permission " + perm + " to be " 1067 + (granted ? "granted" : "revoked")); 1068 } 1069 1070 /** 1071 * Polls until {@code app} is granted or denied the given permission. 1072 */ pollForPermission(TestApp app, String perm, boolean granted)1073 public static void pollForPermission(TestApp app, String perm, boolean granted) 1074 throws Exception { 1075 pollForPermission(app.getPackageName(), perm, granted); 1076 } 1077 1078 /** 1079 * Polls until {@code packageName} is granted or denied the given permission. 1080 */ pollForPermission(String packageName, String perm, boolean granted)1081 public static void pollForPermission(String packageName, String perm, boolean granted) 1082 throws Exception { 1083 pollForCondition( 1084 () -> granted == checkPermission(packageName, perm), 1085 "Timed out while waiting for permission " + perm + " to be " 1086 + (granted ? "granted" : "revoked")); 1087 } 1088 1089 /** 1090 * Returns true iff {@code packageName} is granted a given permission. 1091 */ checkPermission(String packageName, String perm)1092 public static boolean checkPermission(String packageName, String perm) { 1093 try { 1094 int uid = getContext().getPackageManager().getPackageUid(packageName, 0); 1095 1096 Optional<ActivityManager.RunningAppProcessInfo> process = getAppProcessInfo( 1097 packageName); 1098 int pid = process.isPresent() ? process.get().pid : -1; 1099 return checkPermissionAndAppOp(perm, packageName, pid, uid); 1100 } catch (PackageManager.NameNotFoundException e) { 1101 return false; 1102 } 1103 } 1104 1105 /** 1106 * Returns true iff {@code app} is granted a given permission. 1107 */ checkPermission(TestApp app, String perm)1108 public static boolean checkPermission(TestApp app, String perm) { 1109 return checkPermission(app.getPackageName(), perm); 1110 } 1111 1112 /** 1113 * Asserts the entire content of the file equals exactly {@code expectedContent}. 1114 */ assertFileContent(File file, byte[] expectedContent)1115 public static void assertFileContent(File file, byte[] expectedContent) throws IOException { 1116 try (FileInputStream fis = new FileInputStream(file)) { 1117 assertInputStreamContent(fis, expectedContent); 1118 } 1119 } 1120 1121 /** 1122 * Asserts the entire content of the file equals exactly {@code expectedContent}. 1123 * <p>Sets {@code fd} to beginning of file first. 1124 */ assertFileContent(FileDescriptor fd, byte[] expectedContent)1125 public static void assertFileContent(FileDescriptor fd, byte[] expectedContent) 1126 throws IOException, ErrnoException { 1127 Os.lseek(fd, 0, OsConstants.SEEK_SET); 1128 try (FileInputStream fis = new FileInputStream(fd)) { 1129 assertInputStreamContent(fis, expectedContent); 1130 } 1131 } 1132 1133 /** 1134 * Asserts that {@code dir} is a directory and that it doesn't contain any of 1135 * {@code unexpectedContent} 1136 */ assertDirectoryDoesNotContain(@onNull File dir, File... unexpectedContent)1137 public static void assertDirectoryDoesNotContain(@NonNull File dir, File... unexpectedContent) { 1138 assertThat(dir.isDirectory()).isTrue(); 1139 assertThat(Arrays.asList(dir.listFiles())).containsNoneIn(unexpectedContent); 1140 } 1141 1142 /** 1143 * Asserts that {@code dir} is a directory and that it contains all of {@code expectedContent} 1144 */ assertDirectoryContains(@onNull File dir, File... expectedContent)1145 public static void assertDirectoryContains(@NonNull File dir, File... expectedContent) { 1146 assertThat(dir.isDirectory()).isTrue(); 1147 assertThat(Arrays.asList(dir.listFiles())).containsAtLeastElementsIn(expectedContent); 1148 } 1149 getExternalStorageDir()1150 public static File getExternalStorageDir() { 1151 return sExternalStorageDirectory; 1152 } 1153 setExternalStorageVolume(@onNull String volName)1154 public static void setExternalStorageVolume(@NonNull String volName) { 1155 sStorageVolumeName = volName.toLowerCase(Locale.ROOT); 1156 sExternalStorageDirectory = new File("/storage/" + volName); 1157 } 1158 1159 /** 1160 * Resets the root directory of external storage to the default. 1161 * 1162 * @see Environment#getExternalStorageDirectory() 1163 */ resetDefaultExternalStorageVolume()1164 public static void resetDefaultExternalStorageVolume() { 1165 sStorageVolumeName = MediaStore.VOLUME_EXTERNAL; 1166 sExternalStorageDirectory = Environment.getExternalStorageDirectory(); 1167 } 1168 1169 /** 1170 * Asserts the default volume used in helper methods is the primary volume. 1171 */ assertDefaultVolumeIsPrimary()1172 public static void assertDefaultVolumeIsPrimary() { 1173 assertVolumeType(true /* isPrimary */); 1174 } 1175 1176 /** 1177 * Asserts the default volume used in helper methods is a public volume. 1178 */ assertDefaultVolumeIsPublic()1179 public static void assertDefaultVolumeIsPublic() { 1180 assertVolumeType(false /* isPrimary */); 1181 } 1182 1183 /** 1184 * Creates and returns the Android data sub-directory belonging to the calling package. 1185 */ getExternalFilesDir()1186 public static File getExternalFilesDir() { 1187 final String packageName = getContext().getPackageName(); 1188 final File res = new File(getAndroidDataDir(), packageName + "/files"); 1189 if (!res.equals(getContext().getExternalFilesDir(null))) { 1190 res.mkdirs(); 1191 } 1192 return res; 1193 } 1194 1195 /** 1196 * Creates and returns the Android media sub-directory belonging to the calling package. 1197 */ getExternalMediaDir()1198 public static File getExternalMediaDir() { 1199 final String packageName = getContext().getPackageName(); 1200 final File res = new File(getAndroidMediaDir(), packageName); 1201 if (!res.equals(getContext().getExternalMediaDirs()[0])) { 1202 res.mkdirs(); 1203 } 1204 return res; 1205 } 1206 getAlarmsDir()1207 public static File getAlarmsDir() { 1208 return new File(getExternalStorageDir(), 1209 Environment.DIRECTORY_ALARMS); 1210 } 1211 getAndroidDir()1212 public static File getAndroidDir() { 1213 return new File(getExternalStorageDir(), 1214 "Android"); 1215 } 1216 getAudiobooksDir()1217 public static File getAudiobooksDir() { 1218 return new File(getExternalStorageDir(), 1219 Environment.DIRECTORY_AUDIOBOOKS); 1220 } 1221 getDcimDir()1222 public static File getDcimDir() { 1223 return new File(getExternalStorageDir(), Environment.DIRECTORY_DCIM); 1224 } 1225 getDocumentsDir()1226 public static File getDocumentsDir() { 1227 return new File(getExternalStorageDir(), 1228 Environment.DIRECTORY_DOCUMENTS); 1229 } 1230 getDownloadDir()1231 public static File getDownloadDir() { 1232 return new File(getExternalStorageDir(), 1233 Environment.DIRECTORY_DOWNLOADS); 1234 } 1235 getMusicDir()1236 public static File getMusicDir() { 1237 return new File(getExternalStorageDir(), 1238 Environment.DIRECTORY_MUSIC); 1239 } 1240 getMoviesDir()1241 public static File getMoviesDir() { 1242 return new File(getExternalStorageDir(), 1243 Environment.DIRECTORY_MOVIES); 1244 } 1245 getNotificationsDir()1246 public static File getNotificationsDir() { 1247 return new File(getExternalStorageDir(), 1248 Environment.DIRECTORY_NOTIFICATIONS); 1249 } 1250 getPicturesDir()1251 public static File getPicturesDir() { 1252 return new File(getExternalStorageDir(), 1253 Environment.DIRECTORY_PICTURES); 1254 } 1255 getPodcastsDir()1256 public static File getPodcastsDir() { 1257 return new File(getExternalStorageDir(), 1258 Environment.DIRECTORY_PODCASTS); 1259 } 1260 getRecordingsDir()1261 public static File getRecordingsDir() { 1262 return new File(getExternalStorageDir(), 1263 Environment.DIRECTORY_RECORDINGS); 1264 } 1265 getRingtonesDir()1266 public static File getRingtonesDir() { 1267 return new File(getExternalStorageDir(), 1268 Environment.DIRECTORY_RINGTONES); 1269 } 1270 getAndroidDataDir()1271 public static File getAndroidDataDir() { 1272 return new File(getAndroidDir(), "data"); 1273 } 1274 getAndroidMediaDir()1275 public static File getAndroidMediaDir() { 1276 return new File(getAndroidDir(), "media"); 1277 } 1278 getDefaultTopLevelDirs()1279 public static File[] getDefaultTopLevelDirs() { 1280 if (BuildCompat.isAtLeastS()) { 1281 return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(), 1282 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(), 1283 getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRecordingsDir(), 1284 getRingtonesDir()}; 1285 } 1286 return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(), 1287 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(), 1288 getNotificationsDir(), getPicturesDir(), getPodcastsDir(), 1289 getRingtonesDir()}; 1290 } 1291 assertInputStreamContent(InputStream in, byte[] expectedContent)1292 private static void assertInputStreamContent(InputStream in, byte[] expectedContent) 1293 throws IOException { 1294 assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent); 1295 } 1296 1297 /** 1298 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 1299 */ checkPermissionAndAppOp(String permission)1300 private static boolean checkPermissionAndAppOp(String permission) { 1301 final int pid = Os.getpid(); 1302 final int uid = Os.getuid(); 1303 final String packageName = getContext().getPackageName(); 1304 return checkPermissionAndAppOp(permission, packageName, pid, uid); 1305 } 1306 1307 /** 1308 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 1309 */ checkPermissionAndAppOp(String permission, String packageName, int pid, int uid)1310 private static boolean checkPermissionAndAppOp(String permission, String packageName, int pid, 1311 int uid) { 1312 final Context context = getContext(); 1313 if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) { 1314 return false; 1315 } 1316 1317 final String op = AppOpsManager.permissionToOp(permission); 1318 // No AppOp associated with the given permission, skip AppOp check. 1319 if (op == null) { 1320 return true; 1321 } 1322 1323 final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); 1324 try { 1325 appOps.checkPackage(uid, packageName); 1326 } catch (SecurityException e) { 1327 return false; 1328 } 1329 1330 return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; 1331 } 1332 1333 /** 1334 * <p>This method drops shell permission identity. 1335 */ forceStopApp(String packageName)1336 public static void forceStopApp(String packageName) throws Exception { 1337 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 1338 try { 1339 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES); 1340 1341 getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName); 1342 pollForCondition(() -> { 1343 return !isProcessRunning(packageName); 1344 }, "Timed out while waiting for " + packageName + " to be stopped"); 1345 } finally { 1346 uiAutomation.dropShellPermissionIdentity(); 1347 } 1348 } 1349 launchTestApp(TestApp testApp, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)1350 private static void launchTestApp(TestApp testApp, String actionName, 1351 BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent) 1352 throws InterruptedException, TimeoutException { 1353 1354 // Register broadcast receiver 1355 final IntentFilter intentFilter = new IntentFilter(); 1356 intentFilter.addAction(actionName); 1357 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 1358 getContext().registerReceiver(broadcastReceiver, intentFilter); 1359 1360 // Launch the test app. 1361 intent.setPackage(testApp.getPackageName()); 1362 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1363 intent.putExtra(QUERY_TYPE, actionName); 1364 intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName()); 1365 intent.addCategory(Intent.CATEGORY_LAUNCHER); 1366 getContext().startActivity(intent); 1367 if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 1368 final String errorMessage = "Timed out while waiting to receive " + actionName 1369 + " intent from " + testApp.getPackageName(); 1370 throw new TimeoutException(errorMessage); 1371 } 1372 getContext().unregisterReceiver(broadcastReceiver); 1373 } 1374 1375 /** 1376 * Sends intent to {@code testApp} for actions on {@code dirPath} 1377 * 1378 * <p>This method drops shell permission identity. 1379 */ sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1380 private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, 1381 BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception { 1382 if (sShouldForceStopTestApp) { 1383 final String packageName = testApp.getPackageName(); 1384 forceStopApp(packageName); 1385 } 1386 1387 // Launch the test app. 1388 final Intent intent = new Intent(Intent.ACTION_MAIN); 1389 intent.putExtra(INTENT_EXTRA_PATH, dirPath); 1390 launchTestApp(testApp, actionName, broadcastReceiver, latch, intent); 1391 } 1392 1393 /** 1394 * Sends intent to {@code testApp} for actions on {@code uri} 1395 * 1396 * <p>This method drops shell permission identity. 1397 */ sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1398 private static void sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, 1399 BroadcastReceiver broadcastReceiver, CountDownLatch latch) throws Exception { 1400 if (sShouldForceStopTestApp) { 1401 final String packageName = testApp.getPackageName(); 1402 forceStopApp(packageName); 1403 } 1404 1405 final Intent intent = new Intent(Intent.ACTION_MAIN); 1406 intent.putExtra(INTENT_EXTRA_URI, uri); 1407 launchTestApp(testApp, actionName, broadcastReceiver, latch, intent); 1408 } 1409 1410 /** 1411 * Gets images/video metadata from a test app. 1412 * 1413 * <p>This method drops shell permission identity. 1414 */ getMetadataFromTestApp( TestApp testApp, String dirPath, String actionName)1415 private static HashMap<String, String> getMetadataFromTestApp( 1416 TestApp testApp, String dirPath, String actionName) throws Exception { 1417 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1418 return (HashMap<String, String>) bundle.get(actionName); 1419 } 1420 1421 /** 1422 * <p>This method drops shell permission identity. 1423 */ getContentsFromTestApp( TestApp testApp, String dirPath, String actionName)1424 private static ArrayList<String> getContentsFromTestApp( 1425 TestApp testApp, String dirPath, String actionName) throws Exception { 1426 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1427 return bundle.getStringArrayList(actionName); 1428 } 1429 1430 /** 1431 * <p>This method drops shell permission identity. 1432 */ getResultFromTestApp(TestApp testApp, String dirPath, String actionName)1433 private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName) 1434 throws Exception { 1435 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1436 return bundle.getBoolean(actionName, false); 1437 } 1438 getPfdFromTestApp(TestApp testApp, File dirPath, String actionName, String mode)1439 private static ParcelFileDescriptor getPfdFromTestApp(TestApp testApp, File dirPath, 1440 String actionName, String mode) throws Exception { 1441 Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName); 1442 return getContentResolver().openFileDescriptor(bundle.getParcelable(actionName), mode); 1443 } 1444 1445 /** 1446 * <p>This method drops shell permission identity. 1447 */ getFromTestApp(TestApp testApp, String dirPath, String actionName)1448 private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName) 1449 throws Exception { 1450 final CountDownLatch latch = new CountDownLatch(1); 1451 final Bundle[] bundle = new Bundle[1]; 1452 final Exception[] exception = new Exception[1]; 1453 exception[0] = null; 1454 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 1455 @Override 1456 public void onReceive(Context context, Intent intent) { 1457 if (intent.hasExtra(INTENT_EXCEPTION)) { 1458 exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)); 1459 } else { 1460 bundle[0] = intent.getExtras(); 1461 } 1462 latch.countDown(); 1463 } 1464 }; 1465 1466 sendIntentToTestApp(testApp, dirPath, actionName, broadcastReceiver, latch); 1467 if (exception[0] != null) { 1468 throw exception[0]; 1469 } 1470 return bundle[0]; 1471 } 1472 1473 /** 1474 * <p>This method drops shell permission identity. 1475 */ getFromTestApp(TestApp testApp, Uri uri, String actionName)1476 private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName) 1477 throws Exception { 1478 final CountDownLatch latch = new CountDownLatch(1); 1479 final Bundle[] bundle = new Bundle[1]; 1480 final Exception[] exception = new Exception[1]; 1481 exception[0] = null; 1482 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 1483 @Override 1484 public void onReceive(Context context, Intent intent) { 1485 if (intent.hasExtra(INTENT_EXCEPTION)) { 1486 exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)); 1487 } else { 1488 bundle[0] = intent.getExtras(); 1489 } 1490 latch.countDown(); 1491 } 1492 }; 1493 1494 sendIntentToTestApp(testApp, uri, actionName, broadcastReceiver, latch); 1495 if (exception[0] != null) { 1496 throw exception[0]; 1497 } 1498 return bundle[0]; 1499 } 1500 1501 /** 1502 * Sets {@code mode} for the given {@code ops} and the given {@code uid}. 1503 * 1504 * <p>This method drops shell permission identity. 1505 */ setAppOpsModeForUid(int uid, int mode, @NonNull String... ops)1506 public static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) { 1507 adoptShellPermissionIdentity(null); 1508 try { 1509 for (String op : ops) { 1510 getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode); 1511 } 1512 } finally { 1513 dropShellPermissionIdentity(); 1514 } 1515 } 1516 1517 /** 1518 * Queries {@link ContentResolver} for a file IS_PENDING=0 and returns a {@link Cursor} with the 1519 * given columns. 1520 */ 1521 @NonNull queryFileExcludingPending(@onNull File file, String... projection)1522 public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) { 1523 return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName), 1524 file, /*includePending*/ false, projection); 1525 } 1526 1527 @NonNull queryFile(ContentResolver cr, @NonNull File file, String... projection)1528 public static Cursor queryFile(ContentResolver cr, @NonNull File file, String... projection) { 1529 return queryFile(cr, MediaStore.Files.getContentUri(sStorageVolumeName), 1530 file, /*includePending*/ true, projection); 1531 } 1532 1533 @NonNull queryFile(@onNull File file, String... projection)1534 public static Cursor queryFile(@NonNull File file, String... projection) { 1535 return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName), 1536 file, /*includePending*/ true, projection); 1537 } 1538 1539 @NonNull queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, boolean includePending, String... projection)1540 private static Cursor queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, 1541 boolean includePending, String... projection) { 1542 Bundle queryArgs = new Bundle(); 1543 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 1544 MediaStore.MediaColumns.DATA + " = ?"); 1545 queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 1546 new String[] { file.getAbsolutePath() }); 1547 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 1548 1549 if (includePending) { 1550 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 1551 } else { 1552 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE); 1553 } 1554 1555 final Cursor c = cr.query(uri, projection, queryArgs, null); 1556 assertThat(c).isNotNull(); 1557 return c; 1558 } 1559 isObbDirUnmounted()1560 private static boolean isObbDirUnmounted() { 1561 List<String> mounts = new ArrayList<>(); 1562 try { 1563 for (String line : executeShellCommand("cat /proc/mounts").split("\n")) { 1564 String[] split = line.split(" "); 1565 // Only check obb dirs with tmpfs, as if it's mounted for app data 1566 // isolation, it will be tmpfs only. 1567 if (split[0].equals("tmpfs") && split[1].startsWith("/storage/") 1568 && split[1].endsWith("/obb")) { 1569 return false; 1570 } 1571 } 1572 } catch (IOException e) { 1573 Log.e(TAG, "Failed to execute shell command", e); 1574 } 1575 return true; 1576 } 1577 1578 /** 1579 * Creates a new virtual public volume and returns the volume's name. 1580 */ createNewPublicVolume()1581 public static void createNewPublicVolume() throws Exception { 1582 // Unmount data and obb dirs for test app first so test app won't be killed during 1583 // volume unmount. 1584 executeShellCommand("sm unmount-app-data-dirs " + getContext().getPackageName() + " " 1585 + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId()); 1586 pollForCondition(TestUtils::isObbDirUnmounted, 1587 "Timed out while waiting for unmounting obb dir"); 1588 executeShellCommand("sm set-force-adoptable on"); 1589 executeShellCommand("sm set-virtual-disk true"); 1590 Thread.sleep(2000); 1591 pollForCondition(TestUtils::partitionDisk, "Timed out while waiting for disk partitioning"); 1592 } 1593 partitionDisk()1594 private static boolean partitionDisk() { 1595 try { 1596 final String listDisks = executeShellCommand("sm list-disks").trim(); 1597 if (TextUtils.isEmpty(listDisks)) { 1598 return false; 1599 } 1600 executeShellCommand("sm partition " + listDisks + " public"); 1601 return true; 1602 } catch (Exception e) { 1603 return false; 1604 } 1605 } 1606 1607 /** 1608 * Gets the name of the public volume, waiting for a bit for it to be available. 1609 */ getPublicVolumeName()1610 public static String getPublicVolumeName() throws Exception { 1611 final String[] volName = new String[1]; 1612 pollForCondition(() -> { 1613 volName[0] = getCurrentPublicVolumeName(); 1614 return volName[0] != null; 1615 }, "Timed out while waiting for public volume to be ready"); 1616 1617 return volName[0]; 1618 } 1619 1620 /** 1621 * @return the currently mounted public volume, if any. 1622 */ getCurrentPublicVolumeName()1623 public static String getCurrentPublicVolumeName() { 1624 final String[] allVolumeDetails; 1625 try { 1626 allVolumeDetails = executeShellCommand("sm list-volumes") 1627 .trim().split("\n"); 1628 } catch (Exception e) { 1629 Log.e(TAG, "Failed to execute shell command", e); 1630 return null; 1631 } 1632 for (String volDetails : allVolumeDetails) { 1633 if (volDetails.startsWith("public")) { 1634 final String[] publicVolumeDetails = volDetails.trim().split(" "); 1635 String res = publicVolumeDetails[publicVolumeDetails.length - 1]; 1636 if ("null".equals(res)) { 1637 continue; 1638 } 1639 return res; 1640 } 1641 } 1642 return null; 1643 } 1644 1645 /** 1646 * Returns the content URI of the volume on which the test is running. 1647 */ getTestVolumeFileUri()1648 public static Uri getTestVolumeFileUri() { 1649 return MediaStore.Files.getContentUri(sStorageVolumeName); 1650 } 1651 pollForCondition(Supplier<Boolean> condition, String errorMessage)1652 private static void pollForCondition(Supplier<Boolean> condition, String errorMessage) 1653 throws Exception { 1654 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 1655 if (condition.get()) { 1656 return; 1657 } 1658 Thread.sleep(POLLING_SLEEP_MILLIS); 1659 } 1660 throw new TimeoutException(errorMessage); 1661 } 1662 1663 /** 1664 * Polls for all files access to be allowed. 1665 */ pollForManageExternalStorageAllowed()1666 public static void pollForManageExternalStorageAllowed() throws Exception { 1667 pollForCondition( 1668 () -> Environment.isExternalStorageManager(), 1669 "Timed out while waiting for MANAGE_EXTERNAL_STORAGE"); 1670 } 1671 assertVolumeType(boolean isPrimary)1672 private static void assertVolumeType(boolean isPrimary) { 1673 String[] parts = getExternalFilesDir().getAbsolutePath().split("/"); 1674 assertThat(parts.length).isAtLeast(3); 1675 assertThat(parts[1]).isEqualTo("storage"); 1676 if (isPrimary) { 1677 assertThat(parts[2]).isEqualTo("emulated"); 1678 } else { 1679 assertThat(parts[2]).isNotEqualTo("emulated"); 1680 } 1681 } 1682 isProcessRunning(String packageName)1683 private static boolean isProcessRunning(String packageName) { 1684 return getAppProcessInfo(packageName).isPresent(); 1685 } 1686 getAppProcessInfo( String packageName)1687 private static Optional<ActivityManager.RunningAppProcessInfo> getAppProcessInfo( 1688 String packageName) { 1689 return getContext().getSystemService( 1690 ActivityManager.class).getRunningAppProcesses().stream().filter( 1691 p -> packageName.equals(p.processName)).findFirst(); 1692 } 1693 } 1694