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