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