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.provider.MediaStore.VOLUME_EXTERNAL; 20 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY; 21 22 import static androidx.test.InstrumentationRegistry.getContext; 23 24 import static com.google.common.truth.Truth.assertThat; 25 import static com.google.common.truth.Truth.assertWithMessage; 26 27 import static junit.framework.Assert.assertEquals; 28 import static junit.framework.TestCase.assertNotNull; 29 30 import static org.junit.Assert.assertNotEquals; 31 import static org.junit.Assert.fail; 32 33 import android.Manifest; 34 import android.app.Activity; 35 import android.app.ActivityManager; 36 import android.app.AppOpsManager; 37 import android.app.Instrumentation; 38 import android.app.PendingIntent; 39 import android.app.RecoverableSecurityException; 40 import android.app.UiAutomation; 41 import android.content.BroadcastReceiver; 42 import android.content.ContentResolver; 43 import android.content.ContentUris; 44 import android.content.ContentValues; 45 import android.content.Context; 46 import android.content.Intent; 47 import android.content.IntentFilter; 48 import android.content.pm.PackageManager; 49 import android.database.Cursor; 50 import android.net.Uri; 51 import android.os.Bundle; 52 import android.os.Environment; 53 import android.os.IBinder; 54 import android.os.ParcelFileDescriptor; 55 import android.os.Process; 56 import android.os.storage.StorageManager; 57 import android.provider.MediaStore; 58 import android.system.ErrnoException; 59 import android.system.Os; 60 import android.system.OsConstants; 61 import android.text.TextUtils; 62 import android.util.Log; 63 64 import androidx.annotation.NonNull; 65 import androidx.annotation.Nullable; 66 import androidx.core.os.BuildCompat; 67 import androidx.test.InstrumentationRegistry; 68 import androidx.test.uiautomator.UiDevice; 69 import androidx.test.uiautomator.UiObject; 70 import androidx.test.uiautomator.UiObjectNotFoundException; 71 import androidx.test.uiautomator.UiScrollable; 72 import androidx.test.uiautomator.UiSelector; 73 74 import com.android.cts.install.lib.Install; 75 import com.android.cts.install.lib.InstallUtils; 76 import com.android.cts.install.lib.TestApp; 77 import com.android.cts.install.lib.Uninstall; 78 import com.android.modules.utils.build.SdkLevel; 79 80 import com.google.common.io.ByteStreams; 81 82 import org.junit.Assert; 83 84 import java.io.File; 85 import java.io.FileDescriptor; 86 import java.io.FileInputStream; 87 import java.io.IOException; 88 import java.io.InputStream; 89 import java.io.InterruptedIOException; 90 import java.util.ArrayList; 91 import java.util.Arrays; 92 import java.util.HashMap; 93 import java.util.List; 94 import java.util.Locale; 95 import java.util.Optional; 96 import java.util.concurrent.CountDownLatch; 97 import java.util.concurrent.TimeUnit; 98 import java.util.concurrent.TimeoutException; 99 import java.util.function.Supplier; 100 101 /** 102 * General helper functions for ScopedStorageTest tests. 103 */ 104 public class TestUtils { 105 static final String TAG = "ScopedStorageTest"; 106 107 public static final String QUERY_TYPE = "android.scopedstorage.cts.queryType"; 108 public static final String INTENT_EXTRA_PATH = "android.scopedstorage.cts.path"; 109 public static final String INTENT_EXTRA_CONTENT = "android.scopedstorage.cts.content"; 110 public static final String INTENT_EXTRA_URI = "android.scopedstorage.cts.uri"; 111 public static final String INTENT_EXTRA_CALLING_PKG = "android.scopedstorage.cts.calling_pkg"; 112 public static final String INTENT_EXTRA_ARGS = "android.scopedstorage.cts.args"; 113 public static final String INTENT_EXCEPTION = "android.scopedstorage.cts.exception"; 114 public static final String FILE_EXISTS_QUERY = "android.scopedstorage.cts.file_exists"; 115 public static final String CREATE_FILE_QUERY = "android.scopedstorage.cts.createfile"; 116 public static final String CREATE_IMAGE_ENTRY_QUERY = 117 "android.scopedstorage.cts.createimageentry"; 118 public static final String DELETE_FILE_QUERY = "android.scopedstorage.cts.deletefile"; 119 public static final String DELETE_MEDIA_BY_URI_QUERY = 120 "android.scopedstorage.cts.deletemediabyuri"; 121 public static final String DELETE_RECURSIVE_QUERY = "android.scopedstorage.cts.deleteRecursive"; 122 public static final String CAN_OPEN_FILE_FOR_READ_QUERY = 123 "android.scopedstorage.cts.can_openfile_read"; 124 public static final String CAN_OPEN_FILE_FOR_WRITE_QUERY = 125 "android.scopedstorage.cts.can_openfile_write"; 126 public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ = 127 "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_read"; 128 public static final String IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE = 129 "android.scopedstorage.cts.is_uri_redacted_via_file_descriptor_for_write"; 130 public static final String IS_URI_REDACTED_VIA_FILEPATH = 131 "android.scopedstorage.cts.is_uri_redacted_via_filepath"; 132 public static final String QUERY_URI = "android.scopedstorage.cts.query_uri"; 133 public static final String QUERY_MAX_ROW_ID = "android.scopedstorage.cts.query_max_row_id"; 134 public static final String QUERY_MIN_ROW_ID = "android.scopedstorage.cts.query_min_row_id"; 135 public static final String QUERY_OWNER_PACKAGE_NAMES = 136 "android.scopedstorage.cts.query_owner_package_names"; 137 public static final String QUERY_WITH_ARGS = "android.scopedstorage.cts.query_with_args"; 138 public static final String OPEN_FILE_FOR_READ_QUERY = 139 "android.scopedstorage.cts.openfile_read"; 140 public static final String OPEN_FILE_FOR_WRITE_QUERY = 141 "android.scopedstorage.cts.openfile_write"; 142 public static final String CAN_READ_WRITE_QUERY = 143 "android.scopedstorage.cts.can_read_and_write"; 144 public static final String READDIR_QUERY = "android.scopedstorage.cts.readdir"; 145 public static final String SETATTR_QUERY = "android.scopedstorage.cts.setattr"; 146 public static final String CHECK_DATABASE_ROW_EXISTS_QUERY = 147 "android.scopedstorage.cts.check_database_row_exists"; 148 public static final String RENAME_FILE_QUERY = "android.scopedstorage.cts.renamefile"; 149 150 public static final String STR_DATA1 = "Just some random text"; 151 public static final String STR_DATA2 = "More arbitrary stuff"; 152 153 public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes(); 154 public static final byte[] BYTES_DATA2 = STR_DATA2.getBytes(); 155 156 public static final String RENAME_FILE_PARAMS_SEPARATOR = ";"; 157 158 // Root of external storage 159 private static File sExternalStorageDirectory = Environment.getExternalStorageDirectory(); 160 private static String sStorageVolumeName = MediaStore.VOLUME_EXTERNAL; 161 162 /** 163 * Set this to {@code false} if the test is verifying uri grants on testApp. Force stopping the 164 * app will kill the app and it will lose uri grants. 165 */ 166 private static boolean sShouldForceStopTestApp = true; 167 168 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20); 169 private static final long POLLING_SLEEP_MILLIS = 100; 170 171 /** 172 * Creates the top level default directories. 173 * 174 * <p>Those are usually created by MediaProvider, but some naughty tests might delete them 175 * and not restore them afterwards, so we make sure we create them before we make any 176 * assumptions about their existence. 177 */ setupDefaultDirectories()178 public static void setupDefaultDirectories() { 179 for (File dir : getDefaultTopLevelDirs()) { 180 dir.mkdirs(); 181 assertWithMessage("Could not setup default dir [%s]", dir.toString()) 182 .that(dir.exists()) 183 .isTrue(); 184 } 185 } 186 187 /** 188 * Grants {@link Manifest.permission#GRANT_RUNTIME_PERMISSIONS} to the given package. 189 */ grantPermission(String packageName, String permission)190 public static void grantPermission(String packageName, String permission) { 191 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 192 uiAutomation.adoptShellPermissionIdentity("android.permission.GRANT_RUNTIME_PERMISSIONS"); 193 try { 194 uiAutomation.grantRuntimePermission(packageName, permission); 195 } finally { 196 uiAutomation.dropShellPermissionIdentity(); 197 } 198 try { 199 pollForPermission(packageName, permission, true); 200 } catch (Exception e) { 201 fail("Exception on polling for permission grant for " + packageName + " for " 202 + permission + ": " + e.getMessage()); 203 } 204 } 205 206 /** 207 * Revokes permissions from the given package. 208 */ revokePermission(String packageName, String permission)209 public static void revokePermission(String packageName, String permission) { 210 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 211 uiAutomation.adoptShellPermissionIdentity("android.permission.REVOKE_RUNTIME_PERMISSIONS"); 212 try { 213 uiAutomation.revokeRuntimePermission(packageName, permission); 214 } finally { 215 uiAutomation.dropShellPermissionIdentity(); 216 } 217 try { 218 pollForPermission(packageName, permission, false); 219 } catch (Exception e) { 220 fail("Exception on polling for permission revoke for " + packageName + " for " 221 + permission + ": " + e.getMessage()); 222 } 223 } 224 revokeAccessMediaLocation()225 public static void revokeAccessMediaLocation() { 226 revokeAppOpPermission(Manifest.permission.ACCESS_MEDIA_LOCATION, 227 "android:access_media_location"); 228 } 229 230 /** 231 * Revoke the app op for the given permission. Unlike 232 * {@link TestUtils#revokePermission(String, String)}, its usage does not kill the application. 233 * It can be used to drop permissions previously granted to the test application, without 234 * crashing the test application itself. 235 */ revokeAppOpPermission(String manifestPermission, String appOp)236 private static void revokeAppOpPermission(String manifestPermission, String appOp) { 237 try { 238 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() 239 .getUiAutomation() 240 .adoptShellPermissionIdentity("android.permission.MANAGE_APP_OPS_MODES", 241 "android.permission.REVOKE_RUNTIME_PERMISSIONS"); 242 Context context = 243 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() 244 .getTargetContext(); 245 // Revoking the manifest permission will kill the test app. 246 // Deny the permission App Op to revoke this permission. 247 PackageManager packageManager = context.getPackageManager(); 248 String packageName = context.getPackageName(); 249 if (packageManager.checkPermission(manifestPermission, 250 packageName) == PackageManager.PERMISSION_GRANTED) { 251 context.getPackageManager().updatePermissionFlags( 252 manifestPermission, packageName, 253 PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, 254 PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, context.getUser()); 255 context.getSystemService(AppOpsManager.class).setUidMode( 256 appOp, Process.myUid(), 257 AppOpsManager.MODE_IGNORED); 258 } 259 } finally { 260 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() 261 .getUiAutomation() 262 .dropShellPermissionIdentity(); 263 } 264 } 265 266 /** 267 * Adopts shell permission identity for the given permissions. 268 */ adoptShellPermissionIdentity(String... permissions)269 public static void adoptShellPermissionIdentity(String... permissions) { 270 InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( 271 permissions); 272 } 273 274 /** 275 * Drops shell permission identity for all permissions. 276 */ dropShellPermissionIdentity()277 public static void dropShellPermissionIdentity() { 278 InstrumentationRegistry.getInstrumentation().getUiAutomation() 279 .dropShellPermissionIdentity(); 280 } 281 282 /** 283 * Executes a shell command. 284 */ executeShellCommand(String pattern, Object...args)285 public static String executeShellCommand(String pattern, Object...args) throws IOException { 286 String command = String.format(pattern, args); 287 int attempt = 0; 288 while (attempt++ < 5) { 289 try { 290 return executeShellCommandInternal(command); 291 } catch (InterruptedIOException e) { 292 // Hmm, we had trouble executing the shell command; the best we 293 // can do is try again a few more times 294 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 295 } 296 } 297 throw new IOException("Failed to execute " + command); 298 } 299 executeShellCommandInternal(String cmd)300 private static String executeShellCommandInternal(String cmd) throws IOException { 301 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 302 try (FileInputStream output = new FileInputStream( 303 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 304 return new String(ByteStreams.toByteArray(output)); 305 } 306 } 307 308 /** 309 * Makes the given {@code testApp} list the content of the given directory and returns the 310 * result as an {@link ArrayList} 311 */ listAs(TestApp testApp, String dirPath)312 public static ArrayList<String> listAs(TestApp testApp, String dirPath) throws Exception { 313 return getContentsFromTestApp(testApp, dirPath, READDIR_QUERY); 314 } 315 316 /** 317 * Returns {@code true} iff the given {@code path} exists and is readable and 318 * writable for for {@code testApp}. 319 */ canReadAndWriteAs(TestApp testApp, String path)320 public static boolean canReadAndWriteAs(TestApp testApp, String path) throws Exception { 321 return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY); 322 } 323 324 /** 325 * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the 326 * result as an {@link HashMap} 327 */ readExifMetadataFromTestApp( TestApp testApp, String filePath)328 public static HashMap<String, String> readExifMetadataFromTestApp( 329 TestApp testApp, String filePath) throws Exception { 330 HashMap<String, String> res = 331 getMetadataFromTestApp(testApp, filePath, EXIF_METADATA_QUERY); 332 return res; 333 } 334 335 /** 336 * Makes the given {@code testApp} create a file. 337 * 338 * <p>This method drops shell permission identity. 339 */ createFileAs(TestApp testApp, String path)340 public static boolean createFileAs(TestApp testApp, String path) throws Exception { 341 return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY); 342 } 343 344 /** 345 * Makes the given {@code testApp} create a file from the file descriptor passed through binder 346 * 347 * <p>This method drops shell permission identity. 348 */ createFileAs(TestApp testApp, String path, IBinder content)349 public static boolean createFileAs(TestApp testApp, String path, IBinder content) 350 throws Exception { 351 return getResultFromTestApp(testApp, path, CREATE_FILE_QUERY, content); 352 } 353 354 /** 355 * Makes the given {@code testApp} create a mediastore DB entry under 356 * {@code MediaStore.Media.Images}. 357 * 358 * The {@code path} argument is treated as a relative path and a name separated 359 * by an {@code '/'}. 360 */ createImageEntryAs(TestApp testApp, String path)361 public static boolean createImageEntryAs(TestApp testApp, String path) throws Exception { 362 return createImageEntryForUriAs(testApp, path) != null; 363 } 364 365 /** 366 * Makes the given {@code testApp} create a mediastore DB entry under 367 * {@code MediaStore.Media.Images}. 368 * 369 * The {@code path} argument is treated as a relative path and a name separated 370 * by an {@code '/'}. 371 * 372 * Returns URI of the created image. 373 */ createImageEntryForUriAs(TestApp testApp, String path)374 public static Uri createImageEntryForUriAs(TestApp testApp, String path) throws Exception { 375 final String actionName = CREATE_IMAGE_ENTRY_QUERY; 376 final String uriString = getFromTestApp(testApp, path, actionName) 377 .getString(actionName, null); 378 return Uri.parse(uriString); 379 } 380 381 /** 382 * Makes the given {@code testApp} query on {@code uri} to get all the ownerPackageName values. 383 * 384 * <p>This method drops shell permission identity. 385 */ queryForOwnerPackageNamesAs(TestApp testApp, Uri uri)386 public static String[] queryForOwnerPackageNamesAs(TestApp testApp, Uri uri) throws Exception { 387 final String actionName = QUERY_OWNER_PACKAGE_NAMES; 388 return getFromTestApp(testApp, uri, actionName).getStringArray(actionName); 389 } 390 391 /** 392 * Makes the given {@code testApp} query on {@code uri} with the provided {@code queryArgs}. 393 * 394 * Returns the number of rows in the result cursor. 395 * 396 * <p>This method drops shell permission identity. 397 */ queryWithArgsAs(TestApp testApp, Uri uri, Bundle queryArgs)398 public static int queryWithArgsAs(TestApp testApp, Uri uri, Bundle queryArgs) throws Exception { 399 final String actionName = QUERY_WITH_ARGS; 400 return getFromTestApp(testApp, uri, actionName, queryArgs).getInt(actionName); 401 } 402 403 /** 404 * Makes the given {@code testApp} delete media rows by the provided {@code uri}. 405 * 406 * Returns the number of deleted rows. 407 * 408 * <p>This method drops shell permission identity. 409 */ deleteMediaByUriAs(TestApp testApp, Uri uri)410 public static int deleteMediaByUriAs(TestApp testApp, Uri uri) throws Exception { 411 final String actionName = DELETE_MEDIA_BY_URI_QUERY; 412 return getFromTestApp(testApp, uri, actionName).getInt(actionName); 413 } 414 415 /** 416 * Makes the given {@code testApp} delete a file. 417 * 418 * <p>This method drops shell permission identity. 419 */ deleteFileAs(TestApp testApp, String path)420 public static boolean deleteFileAs(TestApp testApp, String path) throws Exception { 421 return getResultFromTestApp(testApp, path, DELETE_FILE_QUERY); 422 } 423 424 /** 425 * Makes the given {@code testApp} delete a file or directory. 426 * If the file is a directory, then deletes all of its children (file or directories) 427 * recursively. 428 * 429 * <p>This method drops shell permission identity. 430 */ deleteRecursivelyAs(TestApp testApp, String path)431 public static boolean deleteRecursivelyAs(TestApp testApp, String path) throws Exception { 432 return getResultFromTestApp(testApp, path, DELETE_RECURSIVE_QUERY); 433 } 434 435 /** 436 * Makes the given {@code testApp} delete a file. Doesn't throw in case of failure. 437 */ deleteFileAsNoThrow(TestApp testApp, String path)438 public static boolean deleteFileAsNoThrow(TestApp testApp, String path) { 439 try { 440 return deleteFileAs(testApp, path); 441 } catch (Exception e) { 442 Log.e(TAG, 443 "Error occurred while deleting file: " + path + " on behalf of app: " + testApp, 444 e); 445 return false; 446 } 447 } 448 449 /** 450 * Makes the given {@code testApp} test {@code file} for existence. 451 * 452 * <p>This method drops shell permission identity. 453 */ fileExistsAs(TestApp testApp, File file)454 public static boolean fileExistsAs(TestApp testApp, File file) 455 throws Exception { 456 return getResultFromTestApp(testApp, file.getPath(), FILE_EXISTS_QUERY); 457 } 458 459 /** 460 * Makes the given {@code testApp} open {@code file} for read or write. 461 * 462 * <p>This method drops shell permission identity. 463 */ canOpenFileAs(TestApp testApp, File file, boolean forWrite)464 public static boolean canOpenFileAs(TestApp testApp, File file, boolean forWrite) 465 throws Exception { 466 String actionName = forWrite ? CAN_OPEN_FILE_FOR_WRITE_QUERY : CAN_OPEN_FILE_FOR_READ_QUERY; 467 return getResultFromTestApp(testApp, file.getPath(), actionName); 468 } 469 470 /** 471 * Makes the given {@code testApp} rename give {@code src} to {@code dst}. 472 * 473 * The method concatenates source and destination paths while sending the request to 474 * {@code testApp}. Hence, {@link TestUtils#RENAME_FILE_PARAMS_SEPARATOR} shouldn't be used 475 * in path names. 476 * 477 * <p>This method drops shell permission identity. 478 */ renameFileAs(TestApp testApp, File src, File dst)479 public static boolean renameFileAs(TestApp testApp, File src, File dst) throws Exception { 480 final String paths = String.format("%s%s%s", 481 src.getAbsolutePath(), RENAME_FILE_PARAMS_SEPARATOR, dst.getAbsolutePath()); 482 return getResultFromTestApp(testApp, paths, RENAME_FILE_QUERY); 483 } 484 485 /** 486 * Makes the given {@code testApp} check if a database row exists for given {@code file} 487 * 488 * <p>This method drops shell permission identity. 489 */ checkDatabaseRowExistsAs(TestApp testApp, File file)490 public static boolean checkDatabaseRowExistsAs(TestApp testApp, File file) throws Exception { 491 return getResultFromTestApp(testApp, file.getPath(), CHECK_DATABASE_ROW_EXISTS_QUERY); 492 } 493 494 /** 495 * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd 496 * redacts EXIF metadata. 497 * 498 * <p> This method drops shell permission identity. 499 */ isFileDescriptorRedacted(TestApp testApp, Uri uri)500 public static boolean isFileDescriptorRedacted(TestApp testApp, Uri uri) 501 throws Exception { 502 String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_READ; 503 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 504 } 505 506 /** 507 * Makes the given {@code testApp} open file descriptor on {@code uri} and verifies that the fd 508 * redacts EXIF metadata. 509 * 510 * <p> This method drops shell permission identity. 511 */ canOpenRedactedUriForWrite(TestApp testApp, Uri uri)512 public static boolean canOpenRedactedUriForWrite(TestApp testApp, Uri uri) 513 throws Exception { 514 String actionName = IS_URI_REDACTED_VIA_FILE_DESCRIPTOR_FOR_WRITE; 515 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 516 } 517 518 519 /** 520 * Makes the given {@code testApp} open file path associated with {@code uri} and verifies that 521 * the path redacts EXIF metadata. 522 * 523 * <p>This method drops shell permission identity. 524 */ isFileOpenRedacted(TestApp testApp, Uri uri)525 public static boolean isFileOpenRedacted(TestApp testApp, Uri uri) 526 throws Exception { 527 final String actionName = IS_URI_REDACTED_VIA_FILEPATH; 528 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 529 } 530 531 /** 532 * Makes the given {@code testApp} query on {@code uri}. 533 * 534 * <p>This method drops shell permission identity. 535 */ canQueryOnUri(TestApp testApp, Uri uri)536 public static boolean canQueryOnUri(TestApp testApp, Uri uri) throws Exception { 537 final String actionName = QUERY_URI; 538 return getFromTestApp(testApp, uri, actionName).getBoolean(actionName, false); 539 } 540 insertFileFromExternalMedia(boolean useRelative)541 public static Uri insertFileFromExternalMedia(boolean useRelative) throws IOException { 542 ContentValues values = new ContentValues(); 543 String filePath = 544 getAndroidMediaDir().toString() + "/" + getContext().getPackageName() + "/" 545 + System.currentTimeMillis(); 546 if (useRelative) { 547 values.put(MediaStore.MediaColumns.RELATIVE_PATH, 548 "Android/media/" + getContext().getPackageName()); 549 values.put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis()); 550 } else { 551 values.put(MediaStore.MediaColumns.DATA, filePath); 552 } 553 554 return getContentResolver().insert( 555 MediaStore.Files.getContentUri(sStorageVolumeName), values); 556 } 557 insertFile(ContentValues values)558 public static void insertFile(ContentValues values) { 559 assertNotNull(getContentResolver().insert( 560 MediaStore.Files.getContentUri(sStorageVolumeName), values)); 561 } 562 updateFile(Uri uri, ContentValues values)563 public static int updateFile(Uri uri, ContentValues values) { 564 return getContentResolver().update(uri, values, new Bundle()); 565 } 566 verifyInsertFromExternalPrivateDirViaRelativePath_denied()567 public static void verifyInsertFromExternalPrivateDirViaRelativePath_denied() throws Exception { 568 // Test that inserting files from Android/obb/.. is not allowed. 569 final String androidObbDir = getExternalObbDir().toString(); 570 ContentValues values = new ContentValues(); 571 values.put( 572 MediaStore.MediaColumns.RELATIVE_PATH, 573 androidObbDir.substring(androidObbDir.indexOf("Android"))); 574 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 575 576 // Test that inserting files from Android/data/.. is not allowed. 577 final String androidDataDir = getExternalFilesDir().toString(); 578 values.put( 579 MediaStore.MediaColumns.RELATIVE_PATH, 580 androidDataDir.substring(androidDataDir.indexOf("Android"))); 581 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 582 } 583 verifyInsertFromExternalMediaDirViaRelativePath_allowed()584 public static void verifyInsertFromExternalMediaDirViaRelativePath_allowed() throws Exception { 585 // Test that inserting files from Android/media/.. is allowed. 586 final String androidMediaDir = getExternalMediaDir().toString(); 587 final ContentValues values = new ContentValues(); 588 values.put( 589 MediaStore.MediaColumns.RELATIVE_PATH, 590 androidMediaDir.substring(androidMediaDir.indexOf("Android"))); 591 insertFile(values); 592 } 593 verifyInsertFromExternalPrivateDirViaData_denied()594 public static void verifyInsertFromExternalPrivateDirViaData_denied() throws Exception { 595 ContentValues values = new ContentValues(); 596 597 // Test that inserting files from Android/obb/.. is not allowed. 598 final String androidObbDir = 599 getExternalObbDir().toString() + "/" + System.currentTimeMillis(); 600 values.put(MediaStore.MediaColumns.DATA, androidObbDir); 601 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 602 603 // Test that inserting files from Android/data/.. is not allowed. 604 final String androidDataDir = getExternalFilesDir().toString(); 605 values.put(MediaStore.MediaColumns.DATA, androidDataDir); 606 assertThrows(IllegalArgumentException.class, () -> insertFile(values)); 607 } 608 verifyInsertFromExternalMediaDirViaData_allowed()609 public static void verifyInsertFromExternalMediaDirViaData_allowed() throws Exception { 610 // Test that inserting files from Android/media/.. is allowed. 611 ContentValues values = new ContentValues(); 612 final String androidMediaDirFile = 613 getExternalMediaDir().toString() + "/" + System.currentTimeMillis(); 614 values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile); 615 insertFile(values); 616 } 617 618 // NOTE: While updating, DATA field should be ignored for all the apps including file manager. verifyUpdateToExternalDirsViaData_denied()619 public static void verifyUpdateToExternalDirsViaData_denied() throws Exception { 620 Uri uri = insertFileFromExternalMedia(false); 621 622 final String androidMediaDirFile = 623 getExternalMediaDir().toString() + "/" + System.currentTimeMillis(); 624 ContentValues values = new ContentValues(); 625 values.put(MediaStore.MediaColumns.DATA, androidMediaDirFile); 626 assertEquals(0, updateFile(uri, values)); 627 628 final String androidObbDir = 629 getExternalObbDir().toString() + "/" + System.currentTimeMillis(); 630 values.put(MediaStore.MediaColumns.DATA, androidObbDir); 631 assertEquals(0, updateFile(uri, values)); 632 633 final String androidDataDir = getExternalFilesDir().toString(); 634 values.put(MediaStore.MediaColumns.DATA, androidDataDir); 635 assertEquals(0, updateFile(uri, values)); 636 } 637 verifyUpdateToExternalMediaDirViaRelativePath_allowed()638 public static void verifyUpdateToExternalMediaDirViaRelativePath_allowed() 639 throws IOException { 640 Uri uri = insertFileFromExternalMedia(true); 641 642 // Test that update to files from Android/media/.. is allowed. 643 final String androidMediaDir = getExternalMediaDir().toString(); 644 ContentValues values = new ContentValues(); 645 values.put( 646 MediaStore.MediaColumns.RELATIVE_PATH, 647 androidMediaDir.substring(androidMediaDir.indexOf("Android"))); 648 assertNotEquals(0, updateFile(uri, values)); 649 } 650 verifyUpdateToExternalPrivateDirsViaRelativePath_denied()651 public static void verifyUpdateToExternalPrivateDirsViaRelativePath_denied() 652 throws Exception { 653 Uri uri = insertFileFromExternalMedia(true); 654 655 // Test that update to files from Android/obb/.. is not allowed. 656 final String androidObbDir = getExternalObbDir().toString(); 657 ContentValues values = new ContentValues(); 658 values.put( 659 MediaStore.MediaColumns.RELATIVE_PATH, 660 androidObbDir.substring(androidObbDir.indexOf("Android"))); 661 assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values)); 662 663 // Test that update to files from Android/data/.. is not allowed. 664 final String androidDataDir = getExternalFilesDir().toString(); 665 values.put( 666 MediaStore.MediaColumns.RELATIVE_PATH, 667 androidDataDir.substring(androidDataDir.indexOf("Android"))); 668 assertThrows(IllegalArgumentException.class, () -> updateFile(uri, values)); 669 } 670 671 /** 672 * Makes the given {@code testApp} open a file for read or write. 673 * 674 * <p>This method drops shell permission identity. 675 */ openFileAs(TestApp testApp, File file, boolean forWrite)676 public static ParcelFileDescriptor openFileAs(TestApp testApp, File file, boolean forWrite) 677 throws Exception { 678 String actionName = forWrite ? OPEN_FILE_FOR_WRITE_QUERY : OPEN_FILE_FOR_READ_QUERY; 679 String mode = forWrite ? "rw" : "r"; 680 return getPfdFromTestApp(testApp, file, actionName, mode); 681 } 682 683 /** 684 * Makes the given {@code testApp} setattr for given file path. 685 * 686 * <p>This method drops shell permission identity. 687 */ setAttrAs(TestApp testApp, String path)688 public static boolean setAttrAs(TestApp testApp, String path) 689 throws Exception { 690 return getResultFromTestApp(testApp, path, SETATTR_QUERY); 691 } 692 693 /** 694 * Installs a {@link TestApp} without storage permissions. 695 */ installApp(TestApp testApp)696 public static void installApp(TestApp testApp) throws Exception { 697 installApp(testApp, /* grantStoragePermission */ false); 698 } 699 700 /** 701 * Installs a {@link TestApp} with storage permissions. 702 */ installAppWithStoragePermissions(TestApp testApp)703 public static void installAppWithStoragePermissions(TestApp testApp) throws Exception { 704 installApp(testApp, /* grantStoragePermission */ true); 705 } 706 707 /** 708 * Installs a {@link TestApp} and may grant it storage permissions. 709 */ installApp(TestApp testApp, boolean grantStoragePermission)710 public static void installApp(TestApp testApp, boolean grantStoragePermission) 711 throws Exception { 712 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 713 try { 714 final String packageName = testApp.getPackageName(); 715 uiAutomation.adoptShellPermissionIdentity( 716 Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES); 717 if (isAppInstalled(testApp)) { 718 Uninstall.packages(packageName); 719 } 720 Install.single(testApp).commit(); 721 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1); 722 if (grantStoragePermission) { 723 grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE); 724 if (SdkLevel.isAtLeastT()) { 725 grantPermission(packageName, Manifest.permission.READ_MEDIA_IMAGES); 726 grantPermission(packageName, Manifest.permission.READ_MEDIA_AUDIO); 727 grantPermission(packageName, Manifest.permission.READ_MEDIA_VIDEO); 728 } 729 } 730 } finally { 731 uiAutomation.dropShellPermissionIdentity(); 732 } 733 } 734 isAppInstalled(TestApp testApp)735 public static boolean isAppInstalled(TestApp testApp) { 736 return InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1; 737 } 738 739 /** 740 * Uninstalls a {@link TestApp}. 741 */ uninstallApp(TestApp testApp)742 public static void uninstallApp(TestApp testApp) throws Exception { 743 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 744 try { 745 final String packageName = testApp.getPackageName(); 746 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.DELETE_PACKAGES); 747 748 Uninstall.packages(packageName); 749 assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(-1); 750 } finally { 751 uiAutomation.dropShellPermissionIdentity(); 752 } 753 } 754 755 /** 756 * Uninstalls a {@link TestApp}. Doesn't throw in case of failure. 757 */ uninstallAppNoThrow(TestApp testApp)758 public static void uninstallAppNoThrow(TestApp testApp) { 759 try { 760 uninstallApp(testApp); 761 } catch (Exception e) { 762 Log.e(TAG, "Exception occurred while uninstalling app: " + testApp, e); 763 } 764 } 765 getContentResolver()766 public static ContentResolver getContentResolver() { 767 return getContext().getContentResolver(); 768 } 769 770 /** 771 * Inserts a file into the database using {@link MediaStore.MediaColumns#DATA}. 772 */ insertFileUsingDataColumn(@onNull File file)773 public static Uri insertFileUsingDataColumn(@NonNull File file) { 774 final ContentValues values = new ContentValues(); 775 values.put(MediaStore.MediaColumns.DATA, file.getPath()); 776 return getContentResolver().insert(MediaStore.Files.getContentUri(sStorageVolumeName), 777 values); 778 } 779 780 /** 781 * Returns the content URI for images based on the current storage volume. 782 */ getImageContentUri()783 public static Uri getImageContentUri() { 784 return MediaStore.Images.Media.getContentUri(sStorageVolumeName); 785 } 786 787 /** 788 * Returns the content URI for videos based on the current storage volume. 789 */ getVideoContentUri()790 public static Uri getVideoContentUri() { 791 return MediaStore.Video.Media.getContentUri(sStorageVolumeName); 792 } 793 794 /** 795 * Renames the given file using {@link ContentResolver} and {@link MediaStore} and APIs. 796 * This method uses the data column, and not all apps can use it. 797 * 798 * @see MediaStore.MediaColumns#DATA 799 */ renameWithMediaProvider(@onNull File oldPath, @NonNull File newPath)800 public static int renameWithMediaProvider(@NonNull File oldPath, @NonNull File newPath) { 801 ContentValues values = new ContentValues(); 802 values.put(MediaStore.MediaColumns.DATA, newPath.getPath()); 803 return getContentResolver().update(MediaStore.Files.getContentUri(sStorageVolumeName), 804 values, /*where*/ MediaStore.MediaColumns.DATA + "=?", 805 /*whereArgs*/ new String[]{oldPath.getPath()}); 806 } 807 808 /** 809 * Queries {@link ContentResolver} for a file and returns the corresponding {@link Uri} for its 810 * entry in the database. Returns {@code null} if file doesn't exist in the database. 811 */ 812 @Nullable getFileUri(@onNull File file)813 public static Uri getFileUri(@NonNull File file) { 814 final Uri contentUri = MediaStore.Files.getContentUri(sStorageVolumeName); 815 final int id = getFileRowIdFromDatabase(file); 816 return id == -1 ? null : ContentUris.withAppendedId(contentUri, id); 817 } 818 819 /** 820 * Queries {@link ContentResolver} for a file and returns the corresponding row ID for its 821 * entry in the database. Returns {@code -1} if file is not found. 822 */ getFileRowIdFromDatabase(@onNull File file)823 public static int getFileRowIdFromDatabase(@NonNull File file) { 824 return getFileRowIdFromDatabase(getContentResolver(), file); 825 } 826 827 /** 828 * Queries given {@link ContentResolver} for a file and returns the corresponding row ID for 829 * its entry in the database. Returns {@code -1} if file is not found. 830 */ getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file)831 public static int getFileRowIdFromDatabase(ContentResolver cr, @NonNull File file) { 832 int id = -1; 833 try (Cursor c = queryFile(cr, file, MediaStore.MediaColumns._ID)) { 834 if (c.moveToFirst()) { 835 id = c.getInt(0); 836 } 837 } 838 return id; 839 } 840 841 /** 842 * Queries {@link ContentResolver} for a file and returns the corresponding owner package name 843 * for its entry in the database. 844 */ 845 @Nullable getFileOwnerPackageFromDatabase(@onNull File file)846 public static String getFileOwnerPackageFromDatabase(@NonNull File file) { 847 String ownerPackage = null; 848 try (Cursor c = queryFile(file, MediaStore.MediaColumns.OWNER_PACKAGE_NAME)) { 849 if (c.moveToFirst()) { 850 ownerPackage = c.getString(0); 851 } 852 } 853 return ownerPackage; 854 } 855 856 /** 857 * Queries {@link ContentResolver} for a file and returns the corresponding file size for its 858 * entry in the database. Returns {@code -1} if file is not found. 859 */ 860 @Nullable getFileSizeFromDatabase(@onNull File file)861 public static int getFileSizeFromDatabase(@NonNull File file) { 862 int size = -1; 863 try (Cursor c = queryFile(file, MediaStore.MediaColumns.SIZE)) { 864 if (c.moveToFirst()) { 865 size = c.getInt(0); 866 } 867 } 868 return size; 869 } 870 871 /** 872 * Queries {@link ContentResolver} for a video file and returns a {@link Cursor} with the given 873 * columns. 874 */ 875 @NonNull queryVideoFile(File file, String... projection)876 public static Cursor queryVideoFile(File file, String... projection) { 877 return queryFile(getContentResolver(), 878 MediaStore.Video.Media.getContentUri(sStorageVolumeName), file, 879 /*includePending*/ true, projection); 880 } 881 882 /** 883 * Queries {@link ContentResolver} for an image file and returns a {@link Cursor} with the given 884 * columns. 885 */ 886 @NonNull queryImageFile(File file, String... projection)887 public static Cursor queryImageFile(File file, String... projection) { 888 return queryFile(getContentResolver(), 889 MediaStore.Images.Media.getContentUri(sStorageVolumeName), file, 890 /*includePending*/ true, projection); 891 } 892 893 /** 894 * Queries {@link ContentResolver} for an audio file and returns a {@link Cursor} with the given 895 * columns. 896 */ 897 @NonNull queryAudioFile(File file, String... projection)898 public static Cursor queryAudioFile(File file, String... projection) { 899 return queryFile(getContentResolver(), 900 MediaStore.Audio.Media.getContentUri(sStorageVolumeName), file, 901 /*includePending*/ true, projection); 902 } 903 904 /** 905 * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its 906 * entry in the database. 907 */ 908 @NonNull getFileMimeTypeFromDatabase(@onNull File file)909 public static String getFileMimeTypeFromDatabase(@NonNull File file) { 910 String mimeType = ""; 911 try (Cursor c = queryFile(file, MediaStore.MediaColumns.MIME_TYPE)) { 912 if (c.moveToFirst()) { 913 mimeType = c.getString(0); 914 } 915 } 916 return mimeType; 917 } 918 919 /** 920 * Sets {@link AppOpsManager#MODE_ALLOWED} for the given {@code ops} and the given {@code uid}. 921 * 922 * <p>This method drops shell permission identity. 923 */ allowAppOpsToUid(int uid, @NonNull String... ops)924 public static void allowAppOpsToUid(int uid, @NonNull String... ops) { 925 setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, ops); 926 } 927 928 /** 929 * Sets {@link AppOpsManager#MODE_ERRORED} for the given {@code ops} and the given {@code uid}. 930 * 931 * <p>This method drops shell permission identity. 932 */ denyAppOpsToUid(int uid, @NonNull String... ops)933 public static void denyAppOpsToUid(int uid, @NonNull String... ops) { 934 setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, ops); 935 } 936 937 /** 938 * Deletes the given file through {@link ContentResolver} and {@link MediaStore} APIs, 939 * and asserts that the file was successfully deleted from the database. 940 */ deleteWithMediaProvider(@onNull File file)941 public static void deleteWithMediaProvider(@NonNull File file) { 942 Bundle extras = new Bundle(); 943 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 944 MediaStore.MediaColumns.DATA + " = ?"); 945 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 946 new String[]{file.getPath()}); 947 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 948 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 949 assertThat(getContentResolver().delete( 950 MediaStore.Files.getContentUri(sStorageVolumeName), extras)).isEqualTo(1); 951 } 952 953 /** 954 * Deletes db rows and files corresponding to uri through {@link ContentResolver} and 955 * {@link MediaStore} APIs. 956 */ deleteWithMediaProviderNoThrow(Uri... uris)957 public static void deleteWithMediaProviderNoThrow(Uri... uris) { 958 for (Uri uri : uris) { 959 if (uri == null) continue; 960 961 try { 962 getContentResolver().delete(uri, Bundle.EMPTY); 963 } catch (Exception ignored) { 964 } 965 } 966 } 967 968 /** 969 * Renames the given file through {@link ContentResolver} and {@link MediaStore} APIs, 970 * and asserts that the file was updated in the database. 971 */ updateDisplayNameWithMediaProvider(Uri uri, String relativePath, String oldDisplayName, String newDisplayName)972 public static void updateDisplayNameWithMediaProvider(Uri uri, String relativePath, 973 String oldDisplayName, String newDisplayName) { 974 String selection = MediaStore.MediaColumns.RELATIVE_PATH + " = ? AND " 975 + MediaStore.MediaColumns.DISPLAY_NAME + " = ?"; 976 String[] selectionArgs = {relativePath + '/', oldDisplayName}; 977 Bundle extras = new Bundle(); 978 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection); 979 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs); 980 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 981 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 982 983 ContentValues values = new ContentValues(); 984 values.put(MediaStore.MediaColumns.DISPLAY_NAME, newDisplayName); 985 986 assertThat(getContentResolver().update(uri, values, extras)).isEqualTo(1); 987 } 988 989 /** 990 * Opens the given file through {@link ContentResolver} and {@link MediaStore} APIs. 991 */ 992 @NonNull openWithMediaProvider(@onNull File file, String mode)993 public static ParcelFileDescriptor openWithMediaProvider(@NonNull File file, String mode) 994 throws Exception { 995 final Uri fileUri = getFileUri(file); 996 assertThat(fileUri).isNotNull(); 997 Log.i(TAG, "Uri: " + fileUri + ". Data: " + file.getPath()); 998 ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(fileUri, mode); 999 assertThat(pfd).isNotNull(); 1000 return pfd; 1001 } 1002 1003 /** 1004 * Opens the given file via file path 1005 */ 1006 @NonNull openWithFilePath(File file, boolean forWrite)1007 public static ParcelFileDescriptor openWithFilePath(File file, boolean forWrite) 1008 throws IOException { 1009 return ParcelFileDescriptor.open(file, 1010 forWrite 1011 ? ParcelFileDescriptor.MODE_READ_WRITE 1012 : ParcelFileDescriptor.MODE_READ_ONLY); 1013 } 1014 1015 /** 1016 * Returns whether we can open the file. 1017 */ canOpen(File file, boolean forWrite)1018 public static boolean canOpen(File file, boolean forWrite) { 1019 try (ParcelFileDescriptor ignore = openWithFilePath(file, forWrite)) { 1020 return true; 1021 } catch (IOException expected) { 1022 return false; 1023 } 1024 } 1025 1026 /** 1027 * Asserts the given operation throws an exception of type {@code T}. 1028 */ assertThrows(Class<T> clazz, Operation<Exception> r)1029 public static <T extends Exception> void assertThrows(Class<T> clazz, Operation<Exception> r) 1030 throws Exception { 1031 assertThrows(clazz, "", r); 1032 } 1033 1034 /** 1035 * Asserts the given operation throws an exception of type {@code T}. 1036 */ assertThrows( Class<T> clazz, String errMsg, Operation<Exception> r)1037 public static <T extends Exception> void assertThrows( 1038 Class<T> clazz, String errMsg, Operation<Exception> r) throws Exception { 1039 try { 1040 r.run(); 1041 fail("Expected " + clazz + " to be thrown"); 1042 } catch (Exception e) { 1043 if (!clazz.isAssignableFrom(e.getClass()) || !e.getMessage().contains(errMsg)) { 1044 Log.e(TAG, "Expected " + clazz + " exception with error message: " + errMsg, e); 1045 throw e; 1046 } 1047 } 1048 } 1049 setShouldForceStopTestApp(boolean value)1050 public static void setShouldForceStopTestApp(boolean value) { 1051 sShouldForceStopTestApp = value; 1052 } 1053 readMaximumRowIdFromDatabaseAs(TestApp app, Uri uri)1054 public static long readMaximumRowIdFromDatabaseAs(TestApp app, Uri uri) throws Exception { 1055 final String actionName = QUERY_MAX_ROW_ID; 1056 return getFromTestApp(app, uri, actionName).getLong(actionName, Long.MIN_VALUE); 1057 } 1058 readMinimumRowIdFromDatabaseAs(TestApp app, Uri uri)1059 public static long readMinimumRowIdFromDatabaseAs(TestApp app, Uri uri) throws Exception { 1060 final String actionName = QUERY_MIN_ROW_ID; 1061 return getFromTestApp(app, uri, actionName).getLong(actionName, Long.MAX_VALUE); 1062 } 1063 doEscalation(RecoverableSecurityException exception)1064 public static void doEscalation(RecoverableSecurityException exception) throws Exception { 1065 doEscalation(exception.getUserAction().getActionIntent()); 1066 } 1067 doEscalation(PendingIntent pi)1068 public static void doEscalation(PendingIntent pi) throws Exception { 1069 doEscalation(pi, true /* allowAccess */, false /* shouldCheckDialogShownValue */, 1070 false /* isDialogShownExpectedExpected */); 1071 } 1072 doEscalation(PendingIntent pi, boolean allowAccess, boolean shouldCheckDialogShownValue, boolean isDialogShownExpected)1073 public static void doEscalation(PendingIntent pi, boolean allowAccess, 1074 boolean shouldCheckDialogShownValue, boolean isDialogShownExpected) throws Exception { 1075 // Try launching the action to grant ourselves access 1076 final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); 1077 final Intent intent = new Intent(inst.getContext(), GetResultActivity.class); 1078 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1079 1080 // Wake up the device and dismiss the keyguard before the test starts 1081 final UiDevice device = UiDevice.getInstance(inst); 1082 device.executeShellCommand("input keyevent KEYCODE_WAKEUP"); 1083 device.executeShellCommand("wm dismiss-keyguard"); 1084 1085 final GetResultActivity activity = (GetResultActivity) inst.startActivitySync(intent); 1086 // Wait for the UI Thread to become idle. 1087 inst.waitForIdleSync(); 1088 activity.clearResult(); 1089 device.waitForIdle(); 1090 activity.startIntentSenderForResult(pi.getIntentSender(), 42, null, 0, 0, 0); 1091 1092 device.waitForIdle(); 1093 final long timeout = 5_000; 1094 if (allowAccess) { 1095 // Some dialogs may have granted access automatically, so we're willing 1096 // to keep rolling forward if we can't find our grant button 1097 final UiSelector grant = new UiSelector().textMatches("(?i)Allow"); 1098 if (isWatch(inst.getContext().getPackageManager())) { 1099 UiScrollable uiScrollable = new UiScrollable(new UiSelector().scrollable(true)); 1100 try { 1101 uiScrollable.scrollIntoView(grant); 1102 } catch (UiObjectNotFoundException e) { 1103 // Scrolling can fail if the UI is not scrollable 1104 } 1105 } 1106 final boolean grantExists = new UiObject(grant).waitForExists(timeout); 1107 1108 if (shouldCheckDialogShownValue) { 1109 assertThat(grantExists).isEqualTo(isDialogShownExpected); 1110 } 1111 1112 if (grantExists) { 1113 device.findObject(grant).click(); 1114 } 1115 final GetResultActivity.Result res = activity.getResult(); 1116 // Verify that we now have access 1117 Assert.assertEquals(Activity.RESULT_OK, res.resultCode); 1118 } else { 1119 // fine the Deny button 1120 final UiSelector deny = new UiSelector().textMatches("(?i)Deny"); 1121 final boolean denyExists = new UiObject(deny).waitForExists(timeout); 1122 1123 assertThat(denyExists).isTrue(); 1124 1125 device.findObject(deny).click(); 1126 1127 final GetResultActivity.Result res = activity.getResult(); 1128 // Verify that we don't have access 1129 Assert.assertEquals(Activity.RESULT_CANCELED, res.resultCode); 1130 } 1131 } 1132 isWatch(PackageManager packageManager)1133 private static boolean isWatch(PackageManager packageManager) { 1134 return hasFeature(packageManager, PackageManager.FEATURE_WATCH); 1135 } 1136 hasFeature(PackageManager packageManager, String feature)1137 private static boolean hasFeature(PackageManager packageManager, String feature) { 1138 return packageManager.hasSystemFeature(feature); 1139 } 1140 1141 /** 1142 * A functional interface representing an operation that takes no arguments, 1143 * returns no arguments and might throw an {@link Exception} of any kind. 1144 * 1145 * @param T the subclass of {@link java.lang.Exception} that this operation might throw. 1146 */ 1147 @FunctionalInterface 1148 public interface Operation<T extends Exception> { 1149 /** 1150 * This is the method that gets called for any object that implements this interface. 1151 */ run()1152 void run() throws T; 1153 } 1154 1155 /** 1156 * Deletes the given file. If the file is a directory, then deletes all of its children (files 1157 * or directories) recursively. 1158 */ deleteRecursively(@onNull File path)1159 public static boolean deleteRecursively(@NonNull File path) { 1160 if (path.isDirectory()) { 1161 for (File child : path.listFiles()) { 1162 if (!deleteRecursively(child)) { 1163 return false; 1164 } 1165 } 1166 } 1167 return path.delete(); 1168 } 1169 1170 /** 1171 * Asserts can rename file. 1172 */ assertCanRenameFile(File oldFile, File newFile)1173 public static void assertCanRenameFile(File oldFile, File newFile) { 1174 assertCanRenameFile(oldFile, newFile, /* checkDB */ true); 1175 } 1176 1177 /** 1178 * Asserts can rename file and optionally checks if the database is updated after rename. 1179 */ assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase)1180 public static void assertCanRenameFile(File oldFile, File newFile, boolean checkDatabase) { 1181 assertThat(oldFile.renameTo(newFile)).isTrue(); 1182 assertThat(oldFile.exists()).isFalse(); 1183 assertThat(newFile.exists()).isTrue(); 1184 if (checkDatabase) { 1185 assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(-1); 1186 assertThat(getFileRowIdFromDatabase(newFile)).isNotEqualTo(-1); 1187 } 1188 } 1189 1190 /** 1191 * Asserts cannot rename file. 1192 */ assertCantRenameFile(File oldFile, File newFile)1193 public static void assertCantRenameFile(File oldFile, File newFile) { 1194 final int rowId = getFileRowIdFromDatabase(oldFile); 1195 assertThat(oldFile.renameTo(newFile)).isFalse(); 1196 assertThat(oldFile.exists()).isTrue(); 1197 assertThat(getFileRowIdFromDatabase(oldFile)).isEqualTo(rowId); 1198 } 1199 1200 /** 1201 * Assert that app cannot insert files in other app's private directories 1202 * 1203 * @param fileName name of the file 1204 * @param throwsExceptionForDataValue Apps like System Gallery for which Data column is not 1205 * respected, will not throw an Exception as the Data value 1206 * is ignored. 1207 * @param otherApp Other test app in whose external private directory we will 1208 * attempt to insert 1209 * @param callingPackageName Calling package name 1210 */ assertCantInsertToOtherPrivateAppDirectories(String fileName, boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)1211 public static void assertCantInsertToOtherPrivateAppDirectories(String fileName, 1212 boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName) 1213 throws Exception { 1214 // Create directory in which the device test will try to insert file to 1215 final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace( 1216 callingPackageName, otherApp.getPackageName())); 1217 final File file = new File(otherAppExternalDataDir, fileName); 1218 String absolutePath = file.getAbsolutePath(); 1219 1220 final ContentValues valuesWithRelativePath = new ContentValues(); 1221 final String absoluteDirectoryPath = otherAppExternalDataDir.getAbsolutePath(); 1222 valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH, 1223 absoluteDirectoryPath.substring(absoluteDirectoryPath.indexOf("Android"))); 1224 valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); 1225 1226 try { 1227 assertThat(createFileAs(otherApp, file.getPath())).isTrue(); 1228 assertCantInsertDataValue(throwsExceptionForDataValue, absolutePath); 1229 assertCantInsertDataValue(throwsExceptionForDataValue, 1230 "/sdcard/" + absolutePath.substring(absolutePath.indexOf("Android"))); 1231 assertCantInsertDataValue(throwsExceptionForDataValue, 1232 "/storage/emulated/0/Pictures/../" 1233 + absolutePath.substring(absolutePath.indexOf("Android"))); 1234 1235 try { 1236 getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1237 valuesWithRelativePath); 1238 fail("File insert expected to fail: " + file); 1239 } catch (IllegalArgumentException expected) { 1240 } 1241 } finally { 1242 deleteFileAsNoThrow(otherApp, file.getPath()); 1243 } 1244 } 1245 assertCantInsertDataValue(boolean throwsExceptionForDataValue, String path)1246 private static void assertCantInsertDataValue(boolean throwsExceptionForDataValue, 1247 String path) throws Exception { 1248 if (throwsExceptionForDataValue) { 1249 assertThrowsErrorOnInsertToOtherAppPrivateDirectories(path); 1250 } else { 1251 insertDataWithValue(path); 1252 try (Cursor c = getContentResolver().query( 1253 MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1254 new String[]{MediaStore.MediaColumns.DATA}, 1255 MediaStore.MediaColumns.DATA + "=?", new String[]{path}, null)) { 1256 assertThat(c.getCount()).isEqualTo(0); 1257 } 1258 } 1259 } 1260 assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path)1261 private static void assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path) 1262 throws Exception { 1263 assertThrows(IllegalArgumentException.class, () -> insertDataWithValue(path)); 1264 } 1265 insertDataWithValue(String path)1266 private static void insertDataWithValue(String path) { 1267 final ContentValues valuesWithData = new ContentValues(); 1268 valuesWithData.put(MediaStore.MediaColumns.DATA, path); 1269 1270 getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1271 valuesWithData); 1272 } 1273 1274 /** 1275 * Assert that app cannot update files in other app's private directories 1276 * 1277 * @param fileName name of the file 1278 * @param throwsExceptionForDataValue Apps like non-legacy System Gallery/MES for which 1279 * Data column is not respected, will not throw an Exception 1280 * as the Data value is ignored. 1281 * @param otherApp Other test app in whose external private directory we will 1282 * attempt to insert 1283 * @param callingPackageName Calling package name 1284 */ assertCantUpdateToOtherPrivateAppDirectories(String fileName, boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName)1285 public static void assertCantUpdateToOtherPrivateAppDirectories(String fileName, 1286 boolean throwsExceptionForDataValue, TestApp otherApp, String callingPackageName) 1287 throws Exception { 1288 // Create priv-app file and add to the database that we will try to update 1289 final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace( 1290 callingPackageName, otherApp.getPackageName())); 1291 final File file = new File(otherAppExternalDataDir, fileName); 1292 try { 1293 assertThat(createFileAs(otherApp, file.getPath())).isTrue(); 1294 MediaStore.scanFile(getContentResolver(), file); 1295 1296 final ContentValues valuesWithData = new ContentValues(); 1297 valuesWithData.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); 1298 try { 1299 int res = getContentResolver().update( 1300 MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1301 valuesWithData, Bundle.EMPTY); 1302 1303 if (throwsExceptionForDataValue) { 1304 fail("File update expected to fail: " + file); 1305 } else { 1306 assertThat(res).isEqualTo(0); 1307 } 1308 } catch (IllegalArgumentException expected) { 1309 } 1310 1311 final ContentValues valuesWithRelativePath = new ContentValues(); 1312 final String path = file.getAbsolutePath(); 1313 valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH, 1314 path.substring(path.indexOf("Android"))); 1315 valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); 1316 try { 1317 getContentResolver().update(MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1318 valuesWithRelativePath, Bundle.EMPTY); 1319 fail("File update expected to fail: " + file); 1320 } catch (IllegalArgumentException expected) { 1321 } 1322 } finally { 1323 deleteFileAsNoThrow(otherApp, file.getPath()); 1324 } 1325 } 1326 1327 /** 1328 * Asserts can rename directory. 1329 */ assertCanRenameDirectory(File oldDirectory, File newDirectory, @Nullable File[] oldFilesList, @Nullable File[] newFilesList)1330 public static void assertCanRenameDirectory(File oldDirectory, File newDirectory, 1331 @Nullable File[] oldFilesList, @Nullable File[] newFilesList) { 1332 assertThat(oldDirectory.renameTo(newDirectory)).isTrue(); 1333 assertThat(oldDirectory.exists()).isFalse(); 1334 assertThat(newDirectory.exists()).isTrue(); 1335 for (File file : oldFilesList != null ? oldFilesList : new File[0]) { 1336 assertThat(file.exists()).isFalse(); 1337 assertThat(getFileRowIdFromDatabase(file)).isEqualTo(-1); 1338 } 1339 for (File file : newFilesList != null ? newFilesList : new File[0]) { 1340 assertThat(file.exists()).isTrue(); 1341 assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1); 1342 } 1343 } 1344 1345 /** 1346 * Asserts cannot rename directory. 1347 */ assertCantRenameDirectory( File oldDirectory, File newDirectory, @Nullable File[] oldFilesList)1348 public static void assertCantRenameDirectory( 1349 File oldDirectory, File newDirectory, @Nullable File[] oldFilesList) { 1350 assertThat(oldDirectory.renameTo(newDirectory)).isFalse(); 1351 assertThat(oldDirectory.exists()).isTrue(); 1352 for (File file : oldFilesList != null ? oldFilesList : new File[0]) { 1353 assertThat(file.exists()).isTrue(); 1354 assertThat(getFileRowIdFromDatabase(file)).isNotEqualTo(-1); 1355 } 1356 } 1357 assertMountMode(String packageName, int uid, int expectedMountMode)1358 public static void assertMountMode(String packageName, int uid, int expectedMountMode) { 1359 adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE"); 1360 try { 1361 final StorageManager storageManager = getContext().getSystemService( 1362 StorageManager.class); 1363 final int actualMountMode = storageManager.getExternalStorageMountMode(uid, 1364 packageName); 1365 assertWithMessage("mount mode (%s=%s, %s=%s) for package %s and uid %s", 1366 expectedMountMode, mountModeToString(expectedMountMode), 1367 actualMountMode, mountModeToString(actualMountMode), 1368 packageName, uid).that(actualMountMode).isEqualTo(expectedMountMode); 1369 } finally { 1370 dropShellPermissionIdentity(); 1371 } 1372 } 1373 mountModeToString(int mountMode)1374 public static String mountModeToString(int mountMode) { 1375 switch (mountMode) { 1376 case 0: 1377 return "EXTERNAL_NONE"; 1378 case 1: 1379 return "DEFAULT"; 1380 case 2: 1381 return "INSTALLER"; 1382 case 3: 1383 return "PASS_THROUGH"; 1384 case 4: 1385 return "ANDROID_WRITABLE"; 1386 default: 1387 return "INVALID(" + mountMode + ")"; 1388 } 1389 } 1390 assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1391 public static void assertCanAccessPrivateAppAndroidDataDir(boolean canAccess, 1392 TestApp testApp, String callingPackage, String fileName) throws Exception { 1393 File[] dataDirs = getContext().getExternalFilesDirs(null); 1394 canReadWriteFilesInDirs(dataDirs, canAccess, testApp, callingPackage, fileName); 1395 } 1396 assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, TestApp testApp, String callingPackage, String fileName)1397 public static void assertCanAccessPrivateAppAndroidObbDir(boolean canAccess, 1398 TestApp testApp, String callingPackage, String fileName) throws Exception { 1399 File[] obbDirs = getContext().getObbDirs(); 1400 canReadWriteFilesInDirs(obbDirs, canAccess, testApp, callingPackage, fileName); 1401 } 1402 canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, String callingPackage, String fileName)1403 private static void canReadWriteFilesInDirs(File[] dirs, boolean canAccess, TestApp testApp, 1404 String callingPackage, String fileName) throws Exception { 1405 for (File dir : dirs) { 1406 final File otherAppExternalDataDir = new File(dir.getPath().replace( 1407 callingPackage, testApp.getPackageName())); 1408 final File file = new File(otherAppExternalDataDir, fileName); 1409 try { 1410 assertThat(file.exists()).isFalse(); 1411 1412 assertThat(createFileAs(testApp, file.getPath())).isTrue(); 1413 if (canAccess) { 1414 assertThat(file.canRead()).isTrue(); 1415 assertThat(file.canWrite()).isTrue(); 1416 } else { 1417 assertThat(file.canRead()).isFalse(); 1418 assertThat(file.canWrite()).isFalse(); 1419 } 1420 } finally { 1421 deleteFileAsNoThrow(testApp, file.getAbsolutePath()); 1422 } 1423 } 1424 } 1425 1426 /** 1427 * Polls for external storage to be mounted. 1428 */ pollForExternalStorageState()1429 public static void pollForExternalStorageState() throws Exception { 1430 pollForCondition( 1431 () -> Environment.getExternalStorageState(getExternalStorageDir()) 1432 .equals(Environment.MEDIA_MOUNTED), 1433 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 1434 } 1435 1436 /** 1437 * Polls until we're granted or denied a given permission. 1438 */ pollForPermission(String perm, boolean granted)1439 public static void pollForPermission(String perm, boolean granted) throws Exception { 1440 pollForCondition(() -> granted == checkPermissionAndAppOp(perm), 1441 "Timed out while waiting for permission " + perm + " to be " 1442 + (granted ? "granted" : "revoked")); 1443 } 1444 1445 /** 1446 * Polls until {@code app} is granted or denied the given permission. 1447 */ pollForPermission(TestApp app, String perm, boolean granted)1448 public static void pollForPermission(TestApp app, String perm, boolean granted) 1449 throws Exception { 1450 pollForPermission(app.getPackageName(), perm, granted); 1451 } 1452 1453 /** 1454 * Polls until {@code packageName} is granted or denied the given permission. 1455 */ pollForPermission(String packageName, String perm, boolean granted)1456 public static void pollForPermission(String packageName, String perm, boolean granted) 1457 throws Exception { 1458 pollForCondition( 1459 () -> granted == checkPermission(packageName, perm), 1460 "Timed out while waiting for permission " + perm + " to be " 1461 + (granted ? "granted" : "revoked")); 1462 } 1463 1464 /** 1465 * Returns true iff {@code packageName} is granted a given permission. 1466 */ checkPermission(String packageName, String perm)1467 public static boolean checkPermission(String packageName, String perm) { 1468 try { 1469 int uid = getContext().getPackageManager().getPackageUid(packageName, 0); 1470 1471 Optional<ActivityManager.RunningAppProcessInfo> process = getAppProcessInfo( 1472 packageName); 1473 int pid = process.isPresent() ? process.get().pid : -1; 1474 return checkPermissionAndAppOp(perm, packageName, pid, uid); 1475 } catch (PackageManager.NameNotFoundException e) { 1476 return false; 1477 } 1478 } 1479 1480 /** 1481 * Returns true iff {@code app} is granted a given permission. 1482 */ checkPermission(TestApp app, String perm)1483 public static boolean checkPermission(TestApp app, String perm) { 1484 return checkPermission(app.getPackageName(), perm); 1485 } 1486 1487 /** 1488 * Asserts the entire content of the file equals exactly {@code expectedContent}. 1489 */ assertFileContent(File file, byte[] expectedContent)1490 public static void assertFileContent(File file, byte[] expectedContent) throws IOException { 1491 try (FileInputStream fis = new FileInputStream(file)) { 1492 assertInputStreamContent(fis, expectedContent); 1493 } 1494 } 1495 1496 /** 1497 * Asserts the entire content of the file equals exactly {@code expectedContent}. 1498 * <p>Sets {@code fd} to beginning of file first. 1499 */ assertFileContent(FileDescriptor fd, byte[] expectedContent)1500 public static void assertFileContent(FileDescriptor fd, byte[] expectedContent) 1501 throws IOException, ErrnoException { 1502 Os.lseek(fd, 0, OsConstants.SEEK_SET); 1503 try (FileInputStream fis = new FileInputStream(fd)) { 1504 assertInputStreamContent(fis, expectedContent); 1505 } 1506 } 1507 1508 /** 1509 * Asserts that {@code dir} is a directory and that it doesn't contain any of 1510 * {@code unexpectedContent} 1511 */ assertDirectoryDoesNotContain(@onNull File dir, File... unexpectedContent)1512 public static void assertDirectoryDoesNotContain(@NonNull File dir, File... unexpectedContent) { 1513 assertThat(dir.isDirectory()).isTrue(); 1514 assertThat(Arrays.asList(dir.listFiles())).containsNoneIn(unexpectedContent); 1515 } 1516 1517 /** 1518 * Asserts that {@code dir} is a directory and that it contains all of {@code expectedContent} 1519 */ assertDirectoryContains(@onNull File dir, File... expectedContent)1520 public static void assertDirectoryContains(@NonNull File dir, File... expectedContent) { 1521 assertThat(dir.isDirectory()).isTrue(); 1522 assertThat(Arrays.asList(dir.listFiles())).containsAtLeastElementsIn(expectedContent); 1523 } 1524 getExternalStorageDir()1525 public static File getExternalStorageDir() { 1526 return sExternalStorageDirectory; 1527 } 1528 setExternalStorageVolume(@onNull String volName)1529 public static void setExternalStorageVolume(@NonNull String volName) { 1530 sStorageVolumeName = volName.toLowerCase(Locale.ROOT); 1531 sExternalStorageDirectory = new File("/storage/" + volName); 1532 } 1533 1534 /** 1535 * Resets the root directory of external storage to the default. 1536 * 1537 * @see Environment#getExternalStorageDirectory() 1538 */ resetDefaultExternalStorageVolume()1539 public static void resetDefaultExternalStorageVolume() { 1540 sStorageVolumeName = MediaStore.VOLUME_EXTERNAL; 1541 sExternalStorageDirectory = Environment.getExternalStorageDirectory(); 1542 } 1543 1544 /** 1545 * Asserts the default volume used in helper methods is the primary volume. 1546 */ assertDefaultVolumeIsPrimary()1547 public static void assertDefaultVolumeIsPrimary() { 1548 assertVolumeType(true /* isPrimary */); 1549 } 1550 1551 /** 1552 * Asserts the default volume used in helper methods is a public volume. 1553 */ assertDefaultVolumeIsPublic()1554 public static void assertDefaultVolumeIsPublic() { 1555 assertVolumeType(false /* isPrimary */); 1556 } 1557 1558 /** 1559 * Creates and returns the Android data sub-directory belonging to the calling package. 1560 */ getExternalFilesDir()1561 public static File getExternalFilesDir() { 1562 final String packageName = getContext().getPackageName(); 1563 final File res = new File(getAndroidDataDir(), packageName + "/files"); 1564 if (!res.equals(getContext().getExternalFilesDir(null))) { 1565 res.mkdirs(); 1566 } 1567 return res; 1568 } 1569 1570 /** 1571 * Creates and returns the Android obb sub-directory belonging to the calling package. 1572 */ getExternalObbDir()1573 public static File getExternalObbDir() { 1574 final String packageName = getContext().getPackageName(); 1575 final File res = new File(getAndroidObbDir(), packageName); 1576 if (!res.equals(getContext().getObbDirs()[0])) { 1577 res.mkdirs(); 1578 } 1579 return res; 1580 } 1581 1582 /** 1583 * Creates and returns the Android media sub-directory belonging to the calling package. 1584 */ getExternalMediaDir()1585 public static File getExternalMediaDir() { 1586 final String packageName = getContext().getPackageName(); 1587 final File res = new File(getAndroidMediaDir(), packageName); 1588 if (!res.equals(getContext().getExternalMediaDirs()[0])) { 1589 res.mkdirs(); 1590 } 1591 return res; 1592 } 1593 getAlarmsDir()1594 public static File getAlarmsDir() { 1595 return new File(getExternalStorageDir(), 1596 Environment.DIRECTORY_ALARMS); 1597 } 1598 getAndroidDir()1599 public static File getAndroidDir() { 1600 return new File(getExternalStorageDir(), 1601 "Android"); 1602 } 1603 getAudiobooksDir()1604 public static File getAudiobooksDir() { 1605 return new File(getExternalStorageDir(), 1606 Environment.DIRECTORY_AUDIOBOOKS); 1607 } 1608 getDcimDir()1609 public static File getDcimDir() { 1610 return new File(getExternalStorageDir(), Environment.DIRECTORY_DCIM); 1611 } 1612 getDocumentsDir()1613 public static File getDocumentsDir() { 1614 return new File(getExternalStorageDir(), 1615 Environment.DIRECTORY_DOCUMENTS); 1616 } 1617 getDownloadDir()1618 public static File getDownloadDir() { 1619 return new File(getExternalStorageDir(), 1620 Environment.DIRECTORY_DOWNLOADS); 1621 } 1622 getMusicDir()1623 public static File getMusicDir() { 1624 return new File(getExternalStorageDir(), 1625 Environment.DIRECTORY_MUSIC); 1626 } 1627 getMoviesDir()1628 public static File getMoviesDir() { 1629 return new File(getExternalStorageDir(), 1630 Environment.DIRECTORY_MOVIES); 1631 } 1632 getNotificationsDir()1633 public static File getNotificationsDir() { 1634 return new File(getExternalStorageDir(), 1635 Environment.DIRECTORY_NOTIFICATIONS); 1636 } 1637 getPicturesDir()1638 public static File getPicturesDir() { 1639 return new File(getExternalStorageDir(), 1640 Environment.DIRECTORY_PICTURES); 1641 } 1642 getPodcastsDir()1643 public static File getPodcastsDir() { 1644 return new File(getExternalStorageDir(), 1645 Environment.DIRECTORY_PODCASTS); 1646 } 1647 getRecordingsDir()1648 public static File getRecordingsDir() { 1649 return new File(getExternalStorageDir(), 1650 Environment.DIRECTORY_RECORDINGS); 1651 } 1652 getRingtonesDir()1653 public static File getRingtonesDir() { 1654 return new File(getExternalStorageDir(), 1655 Environment.DIRECTORY_RINGTONES); 1656 } 1657 getAndroidDataDir()1658 public static File getAndroidDataDir() { 1659 return new File(getAndroidDir(), "data"); 1660 } 1661 getAndroidObbDir()1662 public static File getAndroidObbDir() { 1663 return new File(getAndroidDir(), "obb"); 1664 } 1665 getAndroidMediaDir()1666 public static File getAndroidMediaDir() { 1667 return new File(getAndroidDir(), "media"); 1668 } 1669 getDefaultTopLevelDirs()1670 public static File[] getDefaultTopLevelDirs() { 1671 if (BuildCompat.isAtLeastS()) { 1672 return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(), 1673 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(), 1674 getNotificationsDir(), getPicturesDir(), getPodcastsDir(), getRecordingsDir(), 1675 getRingtonesDir()}; 1676 } 1677 return new File[]{getAlarmsDir(), getAndroidDir(), getAudiobooksDir(), getDcimDir(), 1678 getDocumentsDir(), getDownloadDir(), getMusicDir(), getMoviesDir(), 1679 getNotificationsDir(), getPicturesDir(), getPodcastsDir(), 1680 getRingtonesDir()}; 1681 } 1682 assertInputStreamContent(InputStream in, byte[] expectedContent)1683 private static void assertInputStreamContent(InputStream in, byte[] expectedContent) 1684 throws IOException { 1685 assertThat(ByteStreams.toByteArray(in)).isEqualTo(expectedContent); 1686 } 1687 1688 /** 1689 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 1690 */ checkPermissionAndAppOp(String permission)1691 private static boolean checkPermissionAndAppOp(String permission) { 1692 final int pid = Os.getpid(); 1693 final int uid = Os.getuid(); 1694 final String packageName = getContext().getPackageName(); 1695 return checkPermissionAndAppOp(permission, packageName, pid, uid); 1696 } 1697 1698 /** 1699 * Checks if the given {@code permission} is granted and corresponding AppOp is MODE_ALLOWED. 1700 */ checkPermissionAndAppOp(String permission, String packageName, int pid, int uid)1701 private static boolean checkPermissionAndAppOp(String permission, String packageName, int pid, 1702 int uid) { 1703 final Context context = getContext(); 1704 if (context.checkPermission(permission, pid, uid) != PackageManager.PERMISSION_GRANTED) { 1705 return false; 1706 } 1707 1708 final String op = AppOpsManager.permissionToOp(permission); 1709 // No AppOp associated with the given permission, skip AppOp check. 1710 if (op == null) { 1711 return true; 1712 } 1713 1714 final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); 1715 try { 1716 appOps.checkPackage(uid, packageName); 1717 } catch (SecurityException e) { 1718 return false; 1719 } 1720 1721 return appOps.unsafeCheckOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; 1722 } 1723 1724 /** 1725 * <p>This method drops shell permission identity. 1726 */ forceStopApp(String packageName)1727 public static void forceStopApp(String packageName) throws Exception { 1728 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 1729 try { 1730 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.FORCE_STOP_PACKAGES); 1731 1732 getContext().getSystemService(ActivityManager.class).forceStopPackage(packageName); 1733 pollForCondition(() -> { 1734 return !isProcessRunning(packageName); 1735 }, "Timed out while waiting for " + packageName + " to be stopped"); 1736 } finally { 1737 uiAutomation.dropShellPermissionIdentity(); 1738 } 1739 } 1740 launchTestApp(TestApp testApp, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent)1741 private static void launchTestApp(TestApp testApp, String actionName, 1742 BroadcastReceiver broadcastReceiver, CountDownLatch latch, Intent intent) 1743 throws InterruptedException, TimeoutException { 1744 1745 // Register broadcast receiver 1746 final IntentFilter intentFilter = new IntentFilter(); 1747 intentFilter.addAction(actionName); 1748 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 1749 getContext().registerReceiver(broadcastReceiver, intentFilter, 1750 Context.RECEIVER_EXPORTED_UNAUDITED); 1751 1752 // Launch the test app. 1753 intent.setPackage(testApp.getPackageName()); 1754 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1755 intent.putExtra(QUERY_TYPE, actionName); 1756 intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName()); 1757 intent.addCategory(Intent.CATEGORY_LAUNCHER); 1758 getContext().startActivity(intent); 1759 if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 1760 final String errorMessage = "Timed out while waiting to receive " + actionName 1761 + " intent from " + testApp.getPackageName(); 1762 throw new TimeoutException(errorMessage); 1763 } 1764 getContext().unregisterReceiver(broadcastReceiver); 1765 } 1766 1767 /** 1768 * Sends intent to {@code testApp} for actions on {@code dirPath} 1769 * 1770 * <p>This method drops shell permission identity. 1771 */ sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, IBinder fileDescriptorBinder, BroadcastReceiver broadcastReceiver, CountDownLatch latch)1772 private static void sendIntentToTestApp(TestApp testApp, String dirPath, String actionName, 1773 IBinder fileDescriptorBinder, BroadcastReceiver broadcastReceiver, CountDownLatch latch) 1774 throws Exception { 1775 if (sShouldForceStopTestApp) { 1776 final String packageName = testApp.getPackageName(); 1777 forceStopApp(packageName); 1778 } 1779 1780 // Launch the test app. 1781 final Intent intent = new Intent(Intent.ACTION_MAIN); 1782 intent.putExtra(INTENT_EXTRA_PATH, dirPath); 1783 if (fileDescriptorBinder != null) { 1784 final Bundle bundle = new Bundle(); 1785 bundle.putBinder(INTENT_EXTRA_CONTENT, fileDescriptorBinder); 1786 intent.putExtra(INTENT_EXTRA_CONTENT, bundle); 1787 } 1788 launchTestApp(testApp, actionName, broadcastReceiver, latch, intent); 1789 } 1790 1791 /** 1792 * Sends intent to {@code testApp} for actions on {@code uri} 1793 * 1794 * <p>This method drops shell permission identity. 1795 */ sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, BroadcastReceiver broadcastReceiver, CountDownLatch latch, Bundle args)1796 private static void sendIntentToTestApp(TestApp testApp, Uri uri, String actionName, 1797 BroadcastReceiver broadcastReceiver, CountDownLatch latch, 1798 Bundle args) throws Exception { 1799 if (sShouldForceStopTestApp) { 1800 final String packageName = testApp.getPackageName(); 1801 forceStopApp(packageName); 1802 } 1803 1804 final Intent intent = new Intent(Intent.ACTION_MAIN); 1805 intent.putExtra(INTENT_EXTRA_URI, uri); 1806 intent.putExtra(INTENT_EXTRA_ARGS, args); 1807 launchTestApp(testApp, actionName, broadcastReceiver, latch, intent); 1808 } 1809 1810 /** 1811 * Gets images/video metadata from a test app. 1812 * 1813 * <p>This method drops shell permission identity. 1814 */ getMetadataFromTestApp( TestApp testApp, String dirPath, String actionName)1815 private static HashMap<String, String> getMetadataFromTestApp( 1816 TestApp testApp, String dirPath, String actionName) throws Exception { 1817 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1818 return (HashMap<String, String>) bundle.get(actionName); 1819 } 1820 1821 /** 1822 * <p>This method drops shell permission identity. 1823 */ getContentsFromTestApp( TestApp testApp, String dirPath, String actionName)1824 private static ArrayList<String> getContentsFromTestApp( 1825 TestApp testApp, String dirPath, String actionName) throws Exception { 1826 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1827 return bundle.getStringArrayList(actionName); 1828 } 1829 1830 /** 1831 * <p>This method drops shell permission identity. 1832 */ getResultFromTestApp(TestApp testApp, String dirPath, String actionName)1833 private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName) 1834 throws Exception { 1835 Bundle bundle = getFromTestApp(testApp, dirPath, actionName); 1836 return bundle.getBoolean(actionName, false); 1837 } 1838 1839 /** 1840 * <p>This method drops shell permission identity. 1841 */ getResultFromTestApp(TestApp testApp, String dirPath, String actionName, IBinder fileDescriptorBinder)1842 private static boolean getResultFromTestApp(TestApp testApp, String dirPath, String actionName, 1843 IBinder fileDescriptorBinder) 1844 throws Exception { 1845 Bundle bundle = getFromTestApp(testApp, dirPath, actionName, fileDescriptorBinder); 1846 return bundle.getBoolean(actionName, false); 1847 } 1848 1849 getPfdFromTestApp(TestApp testApp, File dirPath, String actionName, String mode)1850 private static ParcelFileDescriptor getPfdFromTestApp(TestApp testApp, File dirPath, 1851 String actionName, String mode) throws Exception { 1852 Bundle bundle = getFromTestApp(testApp, dirPath.getPath(), actionName); 1853 return getContentResolver().openFileDescriptor(bundle.getParcelable(actionName), mode); 1854 } 1855 1856 /** 1857 * <p>This method drops shell permission identity. 1858 */ getFromTestApp(TestApp testApp, String dirPath, String actionName)1859 private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName) 1860 throws Exception { 1861 return getFromTestApp(testApp, dirPath, actionName, null); 1862 } 1863 1864 /** 1865 * <p>This method drops shell permission identity. 1866 */ getFromTestApp(TestApp testApp, String dirPath, String actionName, @Nullable IBinder fileDescriptorBinder)1867 private static Bundle getFromTestApp(TestApp testApp, String dirPath, String actionName, 1868 @Nullable IBinder fileDescriptorBinder) 1869 throws Exception { 1870 final CountDownLatch latch = new CountDownLatch(1); 1871 final Bundle[] bundle = new Bundle[1]; 1872 final Exception[] exception = new Exception[1]; 1873 exception[0] = null; 1874 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 1875 @Override 1876 public void onReceive(Context context, Intent intent) { 1877 if (intent.hasExtra(INTENT_EXCEPTION)) { 1878 exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)); 1879 } else { 1880 bundle[0] = intent.getExtras(); 1881 } 1882 latch.countDown(); 1883 } 1884 }; 1885 1886 sendIntentToTestApp(testApp, dirPath, actionName, fileDescriptorBinder, broadcastReceiver, 1887 latch); 1888 if (exception[0] != null) { 1889 throw exception[0]; 1890 } 1891 return bundle[0]; 1892 } 1893 1894 /** 1895 * <p>This method drops shell permission identity. 1896 */ getFromTestApp(TestApp testApp, Uri uri, String actionName)1897 private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName) 1898 throws Exception { 1899 return getFromTestApp(testApp, uri, actionName, null); 1900 } 1901 1902 /** 1903 * <p>This method drops shell permission identity. 1904 */ getFromTestApp(TestApp testApp, Uri uri, String actionName, Bundle args)1905 private static Bundle getFromTestApp(TestApp testApp, Uri uri, String actionName, Bundle args) 1906 throws Exception { 1907 final CountDownLatch latch = new CountDownLatch(1); 1908 final Bundle[] bundle = new Bundle[1]; 1909 final Exception[] exception = new Exception[1]; 1910 exception[0] = null; 1911 BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 1912 @Override 1913 public void onReceive(Context context, Intent intent) { 1914 if (intent.hasExtra(INTENT_EXCEPTION)) { 1915 exception[0] = (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)); 1916 } else { 1917 bundle[0] = intent.getExtras(); 1918 } 1919 latch.countDown(); 1920 } 1921 }; 1922 1923 sendIntentToTestApp(testApp, uri, actionName, broadcastReceiver, latch, args); 1924 if (exception[0] != null) { 1925 throw exception[0]; 1926 } 1927 return bundle[0]; 1928 } 1929 1930 /** 1931 * Sets {@code mode} for the given {@code ops} and the given {@code uid}. 1932 * 1933 * <p>This method drops shell permission identity. 1934 */ setAppOpsModeForUid(int uid, int mode, @NonNull String... ops)1935 public static void setAppOpsModeForUid(int uid, int mode, @NonNull String... ops) { 1936 adoptShellPermissionIdentity(null); 1937 try { 1938 for (String op : ops) { 1939 getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode); 1940 } 1941 } finally { 1942 dropShellPermissionIdentity(); 1943 } 1944 } 1945 1946 /** 1947 * Queries {@link ContentResolver} for a file IS_PENDING=0 and returns a {@link Cursor} with the 1948 * given columns. 1949 */ 1950 @NonNull queryFileExcludingPending(@onNull File file, String... projection)1951 public static Cursor queryFileExcludingPending(@NonNull File file, String... projection) { 1952 return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName), 1953 file, /*includePending*/ false, projection); 1954 } 1955 1956 @NonNull queryFile(ContentResolver cr, @NonNull File file, String... projection)1957 public static Cursor queryFile(ContentResolver cr, @NonNull File file, String... projection) { 1958 return queryFile(cr, MediaStore.Files.getContentUri(sStorageVolumeName), 1959 file, /*includePending*/ true, projection); 1960 } 1961 1962 @NonNull queryFile(@onNull File file, String... projection)1963 public static Cursor queryFile(@NonNull File file, String... projection) { 1964 return queryFile(getContentResolver(), MediaStore.Files.getContentUri(sStorageVolumeName), 1965 file, /*includePending*/ true, projection); 1966 } 1967 1968 @NonNull queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, boolean includePending, String... projection)1969 private static Cursor queryFile(ContentResolver cr, @NonNull Uri uri, @NonNull File file, 1970 boolean includePending, String... projection) { 1971 Bundle queryArgs = new Bundle(); 1972 queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 1973 MediaStore.MediaColumns.DATA + " = ?"); 1974 queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 1975 new String[]{file.getAbsolutePath()}); 1976 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 1977 1978 if (includePending) { 1979 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 1980 } else { 1981 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_EXCLUDE); 1982 } 1983 1984 final Cursor c = cr.query(uri, projection, queryArgs, null); 1985 assertThat(c).isNotNull(); 1986 return c; 1987 } 1988 isObbDirUnmounted()1989 private static boolean isObbDirUnmounted() { 1990 List<String> mounts = new ArrayList<>(); 1991 try { 1992 for (String line : executeShellCommand("cat /proc/mounts").split("\n")) { 1993 String[] split = line.split(" "); 1994 // Only check obb dirs with tmpfs, as if it's mounted for app data 1995 // isolation, it will be tmpfs only. 1996 if (split[0].equals("tmpfs") && split[1].startsWith("/storage/") 1997 && split[1].endsWith("/obb")) { 1998 return false; 1999 } 2000 } 2001 } catch (IOException e) { 2002 Log.e(TAG, "Failed to execute shell command", e); 2003 } 2004 return true; 2005 } 2006 isVolumeMounted(String type)2007 private static boolean isVolumeMounted(String type) { 2008 try { 2009 final String volume = executeShellCommand("sm list-volumes " + type).trim(); 2010 return volume != null && volume.contains(" mounted"); 2011 } catch (Exception e) { 2012 return false; 2013 } 2014 } 2015 isPublicVolumeMounted()2016 private static boolean isPublicVolumeMounted() { 2017 return isVolumeMounted("public"); 2018 } 2019 isEmulatedVolumeMounted()2020 private static boolean isEmulatedVolumeMounted() { 2021 return isVolumeMounted("emulated"); 2022 } 2023 isFuseReady()2024 private static boolean isFuseReady() { 2025 for (String volumeName : MediaStore.getExternalVolumeNames(getContext())) { 2026 final Uri uri = MediaStore.Files.getContentUri(volumeName); 2027 try (Cursor c = getContentResolver().query(uri, null, null, null)) { 2028 assertThat(c).isNotNull(); 2029 } catch (IllegalArgumentException e) { 2030 return false; 2031 } 2032 } 2033 return true; 2034 } 2035 2036 /** 2037 * Prepare or create a public volume for testing 2038 */ preparePublicVolume()2039 public static void preparePublicVolume() throws Exception { 2040 if (getCurrentPublicVolumeName() == null) { 2041 createNewPublicVolume(); 2042 return; 2043 } 2044 2045 if (!Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim())) { 2046 unmountAppDirs(); 2047 // ensure the volume is visible 2048 executeShellCommand("sm set-force-adoptable on"); 2049 Thread.sleep(2000); 2050 pollForCondition(TestUtils::isPublicVolumeMounted, 2051 "Timed out while waiting for public volume"); 2052 pollForCondition(TestUtils::isEmulatedVolumeMounted, 2053 "Timed out while waiting for emulated volume"); 2054 pollForCondition(TestUtils::isFuseReady, 2055 "Timed out while waiting for fuse"); 2056 } 2057 } 2058 isAdoptableStorageSupported()2059 public static boolean isAdoptableStorageSupported() throws Exception { 2060 return hasAdoptableStorageFeature() || hasAdoptableStorageFstab(); 2061 } 2062 hasAdoptableStorageFstab()2063 private static boolean hasAdoptableStorageFstab() throws Exception { 2064 return Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim()); 2065 } 2066 hasAdoptableStorageFeature()2067 private static boolean hasAdoptableStorageFeature() throws Exception { 2068 return getContext().getPackageManager().hasSystemFeature( 2069 PackageManager.FEATURE_ADOPTABLE_STORAGE); 2070 } 2071 2072 /** 2073 * Unmount app's obb and data dirs. 2074 */ unmountAppDirs()2075 public static void unmountAppDirs() throws Exception { 2076 if (TestUtils.isObbDirUnmounted()) { 2077 return; 2078 } 2079 executeShellCommand("sm unmount-app-data-dirs " + getContext().getPackageName() + " " 2080 + android.os.Process.myPid() + " " + android.os.UserHandle.myUserId()); 2081 pollForCondition(TestUtils::isObbDirUnmounted, 2082 "Timed out while waiting for unmounting obb dir"); 2083 } 2084 2085 /** 2086 * Creates a new virtual public volume and returns the volume's name. 2087 */ createNewPublicVolume()2088 public static void createNewPublicVolume() throws Exception { 2089 // Unmount data and obb dirs for test app first so test app won't be killed during 2090 // volume unmount. 2091 unmountAppDirs(); 2092 executeShellCommand("sm set-force-adoptable on"); 2093 executeShellCommand("sm set-virtual-disk true"); 2094 Thread.sleep(2000); 2095 pollForCondition(TestUtils::partitionDisk, "Timed out while waiting for disk partitioning"); 2096 } 2097 partitionDisk()2098 private static boolean partitionDisk() { 2099 try { 2100 final String listDisks = executeShellCommand("sm list-disks").trim(); 2101 if (TextUtils.isEmpty(listDisks)) { 2102 return false; 2103 } 2104 executeShellCommand("sm partition " + listDisks + " public"); 2105 return true; 2106 } catch (Exception e) { 2107 return false; 2108 } 2109 } 2110 2111 /** 2112 * Gets the name of the public volume, waiting for a bit for it to be available. 2113 */ getPublicVolumeName()2114 public static String getPublicVolumeName() throws Exception { 2115 final String[] volName = new String[1]; 2116 pollForCondition(() -> { 2117 volName[0] = getCurrentPublicVolumeName(); 2118 return volName[0] != null; 2119 }, "Timed out while waiting for public volume to be ready"); 2120 2121 return volName[0]; 2122 } 2123 2124 /** 2125 * @return the currently mounted public volume, if any. 2126 */ getCurrentPublicVolumeName()2127 public static String getCurrentPublicVolumeName() { 2128 final String[] allVolumeDetails; 2129 try { 2130 allVolumeDetails = executeShellCommand("sm list-volumes") 2131 .trim().split("\n"); 2132 } catch (Exception e) { 2133 Log.e(TAG, "Failed to execute shell command", e); 2134 return null; 2135 } 2136 for (String volDetails : allVolumeDetails) { 2137 if (volDetails.startsWith("public")) { 2138 final String[] publicVolumeDetails = volDetails.trim().split(" "); 2139 String res = publicVolumeDetails[publicVolumeDetails.length - 1]; 2140 if ("null".equals(res)) { 2141 continue; 2142 } 2143 return res; 2144 } 2145 } 2146 return null; 2147 } 2148 2149 /** 2150 * Returns the content URI of the volume on which the test is running. 2151 */ getTestVolumeFileUri()2152 public static Uri getTestVolumeFileUri() { 2153 return MediaStore.Files.getContentUri(sStorageVolumeName); 2154 } 2155 pollForCondition(Supplier<Boolean> condition, String errorMessage)2156 private static void pollForCondition(Supplier<Boolean> condition, String errorMessage) 2157 throws Exception { 2158 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 2159 if (condition.get()) { 2160 return; 2161 } 2162 Thread.sleep(POLLING_SLEEP_MILLIS); 2163 } 2164 throw new TimeoutException(errorMessage); 2165 } 2166 2167 /** 2168 * Polls for all files access to be allowed. 2169 */ pollForManageExternalStorageAllowed()2170 public static void pollForManageExternalStorageAllowed() throws Exception { 2171 pollForCondition( 2172 () -> Environment.isExternalStorageManager(), 2173 "Timed out while waiting for MANAGE_EXTERNAL_STORAGE"); 2174 } 2175 assertVolumeType(boolean isPrimary)2176 private static void assertVolumeType(boolean isPrimary) { 2177 String[] parts = getExternalFilesDir().getAbsolutePath().split("/"); 2178 assertThat(parts.length).isAtLeast(3); 2179 assertThat(parts[1]).isEqualTo("storage"); 2180 if (isPrimary) { 2181 assertThat(parts[2]).isEqualTo("emulated"); 2182 } else { 2183 assertThat(parts[2]).isNotEqualTo("emulated"); 2184 } 2185 } 2186 isProcessRunning(String packageName)2187 private static boolean isProcessRunning(String packageName) { 2188 return getAppProcessInfo(packageName).isPresent(); 2189 } 2190 getAppProcessInfo( String packageName)2191 private static Optional<ActivityManager.RunningAppProcessInfo> getAppProcessInfo( 2192 String packageName) { 2193 return getContext().getSystemService(ActivityManager.class) 2194 .getRunningAppProcesses() 2195 .stream() 2196 .filter(p -> packageName.equals(p.processName)) 2197 .findFirst(); 2198 } 2199 trashFileAndAssert(Uri uri)2200 public static void trashFileAndAssert(Uri uri) { 2201 final ContentValues values = new ContentValues(); 2202 values.put(MediaStore.MediaColumns.IS_TRASHED, 1); 2203 assertWithMessage("Result of ContentResolver#update for " + uri + " with values to trash " 2204 + "file " + values) 2205 .that(getContentResolver().update(uri, values, Bundle.EMPTY)).isEqualTo(1); 2206 } 2207 untrashFileAndAssert(Uri uri)2208 public static void untrashFileAndAssert(Uri uri) { 2209 final ContentValues values = new ContentValues(); 2210 values.put(MediaStore.MediaColumns.IS_TRASHED, 0); 2211 assertWithMessage("Result of ContentResolver#update for " + uri + " with values to untrash " 2212 + "file " + values) 2213 .that(getContentResolver().update(uri, values, Bundle.EMPTY)).isEqualTo(1); 2214 } 2215 waitForMountedAndIdleState(ContentResolver resolver)2216 public static void waitForMountedAndIdleState(ContentResolver resolver) throws Exception { 2217 // We purposefully perform these operations twice in this specific 2218 // order, since clearing the data on a package can asynchronously 2219 // perform a vold reset, which can make us think storage is ready and 2220 // mounted when it's moments away from being torn down. 2221 pollForExternalStorageMountedState(); 2222 MediaStore.waitForIdle(resolver); 2223 pollForExternalStorageMountedState(); 2224 MediaStore.waitForIdle(resolver); 2225 } 2226 pollForExternalStorageMountedState()2227 private static void pollForExternalStorageMountedState() throws Exception { 2228 final File target = Environment.getExternalStorageDirectory(); 2229 pollForCondition(() -> isExternalStorageDirectoryMounted(target), 2230 "Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 2231 } 2232 isExternalStorageDirectoryMounted(File target)2233 private static boolean isExternalStorageDirectoryMounted(File target) { 2234 boolean isMounted = Environment.MEDIA_MOUNTED.equals( 2235 Environment.getExternalStorageState(target)); 2236 if (isMounted) { 2237 try { 2238 return Os.statvfs(target.getAbsolutePath()).f_blocks > 0; 2239 } catch (Exception e) { 2240 // Waiting for external storage to be mounted 2241 } 2242 } 2243 return false; 2244 } 2245 } 2246