1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media.photopicker; 18 19 import static android.provider.CloudMediaProviderContract.AlbumColumns; 20 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA; 21 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS; 22 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES; 23 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS; 24 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS; 25 import static android.provider.CloudMediaProviderContract.MediaColumns; 26 import static android.provider.MediaStore.VOLUME_EXTERNAL; 27 28 import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY; 29 import static com.android.providers.media.util.MimeUtils.isImageMimeType; 30 import static com.android.providers.media.util.MimeUtils.isVideoMimeType; 31 32 import static com.google.common.truth.Truth.assertThat; 33 import static com.google.common.truth.Truth.assertWithMessage; 34 35 import android.Manifest; 36 import android.app.Instrumentation; 37 import android.app.UiAutomation; 38 import android.content.ContentResolver; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.res.AssetFileDescriptor; 42 import android.database.Cursor; 43 import android.net.Uri; 44 import android.os.Bundle; 45 import android.os.Environment; 46 import android.os.ParcelFileDescriptor; 47 import android.provider.CloudMediaProviderContract; 48 import android.provider.MediaStore; 49 import android.util.Log; 50 import android.util.Pair; 51 52 import androidx.test.InstrumentationRegistry; 53 54 import com.android.providers.media.IsolatedContext; 55 import com.android.providers.media.PickerProviderMediaGenerator; 56 import com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator; 57 import com.android.providers.media.TestConfigStore; 58 import com.android.providers.media.cloudproviders.CloudProviderPrimary; 59 import com.android.providers.media.photopicker.data.ItemsProvider; 60 import com.android.providers.media.photopicker.data.model.Category; 61 import com.android.providers.media.photopicker.data.model.UserId; 62 63 import com.google.common.io.ByteStreams; 64 65 import org.junit.After; 66 import org.junit.Before; 67 import org.junit.Test; 68 69 import java.io.File; 70 import java.io.FileInputStream; 71 import java.io.FileOutputStream; 72 import java.io.IOException; 73 import java.io.InterruptedIOException; 74 import java.util.ArrayList; 75 import java.util.Arrays; 76 import java.util.List; 77 import java.util.function.Consumer; 78 79 public class ItemsProviderTest { 80 /** 81 * To help avoid flaky tests, give ourselves a unique nonce to be used for 82 * all filesystem paths, so that we don't risk conflicting with previous 83 * test runs. 84 */ 85 private static final String NONCE = String.valueOf(System.nanoTime()); 86 private static final String TAG = "ItemsProviderTest"; 87 private static final String VIDEO_FILE_NAME = TAG + "_file_" + NONCE + ".mp4"; 88 private static final String IMAGE_FILE_NAME = TAG + "_file_" + NONCE + ".jpg"; 89 private static final String HIDDEN_DIR_NAME = TAG + "_hidden_dir_" + NONCE; 90 91 private static final Instrumentation sInstrumentation = 92 InstrumentationRegistry.getInstrumentation(); 93 private static final Context sTargetContext = sInstrumentation.getTargetContext(); 94 // We are in a self-instrumentation test - MediaProviderTests - so "target" package name and 95 // "own" package are the same: com.android.providers.media.tests. 96 private static final String sTargetPackageName = sTargetContext.getPackageName(); 97 private ContentResolver mIsolatedResolver; 98 private ItemsProvider mItemsProvider; 99 private TestConfigStore mConfigStore; 100 101 @Before setUp()102 public void setUp() throws Exception { 103 final UiAutomation uiAutomation = sInstrumentation.getUiAutomation(); 104 uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, 105 Manifest.permission.READ_COMPAT_CHANGE_CONFIG, 106 Manifest.permission.READ_DEVICE_CONFIG, 107 Manifest.permission.INTERACT_ACROSS_USERS); 108 109 mConfigStore = new TestConfigStore(); 110 // Remove sync delay to avoid flaky tests 111 mConfigStore.setPickerSyncDelayMs(0); 112 113 final Context isolatedContext = new IsolatedContext(sTargetContext, /* tag */ "databases", 114 /* asFuseThread */ false, sTargetContext.getUser(), mConfigStore); 115 mIsolatedResolver = isolatedContext.getContentResolver(); 116 mItemsProvider = new ItemsProvider(isolatedContext); 117 118 // Wait for MediaStore to be Idle to reduce flakes caused by database updates 119 MediaStore.waitForIdle(mIsolatedResolver); 120 } 121 122 @After tearDown()123 public void tearDown() throws Exception { 124 setCloudProvider(null); 125 } 126 127 /** 128 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 129 * about {@link #ALBUM_ID_CAMERA}. 130 */ 131 @Test testGetCategories_camera()132 public void testGetCategories_camera() throws Exception { 133 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 134 assertThat(c.getCount()).isEqualTo(0); 135 136 // Create 1 image file in Camera dir to test 137 final File cameraDir = getCameraDir(); 138 File imageFile = assertCreateNewImage(cameraDir); 139 try { 140 assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 1); 141 } finally { 142 imageFile.delete(); 143 } 144 } 145 146 /** 147 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 148 * about {@link #ALBUM_ID_CAMERA}. 149 */ 150 @Test testGetCategories_not_camera()151 public void testGetCategories_not_camera() throws Exception { 152 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 153 assertThat(c.getCount()).isEqualTo(0); 154 155 // negative test case: image file which should not be returned in Camera category 156 final File picturesDir = getPicturesDir(); 157 File nonCameraImageFile = assertCreateNewImage(picturesDir); 158 try { 159 assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 0); 160 } finally { 161 nonCameraImageFile.delete(); 162 } 163 } 164 165 /** 166 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 167 * about {@link #ALBUM_ID_VIDEOS}. 168 */ 169 @Test testGetCategories_videos()170 public void testGetCategories_videos() throws Exception { 171 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 172 assertThat(c.getCount()).isEqualTo(0); 173 174 // Create 1 video file in Movies dir to test 175 final File moviesDir = getMoviesDir(); 176 File videoFile = assertCreateNewVideo(moviesDir); 177 try { 178 assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 1); 179 } finally { 180 videoFile.delete(); 181 } 182 } 183 184 /** 185 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 186 * about {@link #ALBUM_ID_VIDEOS}. 187 */ 188 @Test testGetCategories_not_videos()189 public void testGetCategories_not_videos() throws Exception { 190 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 191 assertThat(c.getCount()).isEqualTo(0); 192 193 // negative test case: image file which should not be returned in Videos category 194 final File picturesDir = getPicturesDir(); 195 File imageFile = assertCreateNewImage(picturesDir); 196 try { 197 assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 0); 198 } finally { 199 imageFile.delete(); 200 } 201 } 202 203 /** 204 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 205 * about {@link #ALBUM_ID_SCREENSHOTS}. 206 */ 207 @Test testGetCategories_screenshots()208 public void testGetCategories_screenshots() throws Exception { 209 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 210 assertThat(c.getCount()).isEqualTo(0); 211 212 // Create 1 image file in Screenshots dir to test 213 final File screenshotsDir = getScreenshotsDir(); 214 File imageFile = assertCreateNewImage(screenshotsDir); 215 // Create 1 image file in Screenshots dir of Downloads dir 216 final File screenshotsDirInDownloadsDir = getScreenshotsDirFromDownloadsDir(); 217 File imageFileInScreenshotDirInDownloads = 218 assertCreateNewImage(screenshotsDirInDownloadsDir); 219 220 // Add a top level /Screenshots directory and add a test image inside of it. 221 createTestScreenshotImages(); 222 223 // This file should not be included since it's not a valid screenshot directory, even though 224 // it looks like one. 225 final File myAlbumScreenshotsDir = 226 new File(getPicturesDir(), "MyAlbum" + Environment.DIRECTORY_SCREENSHOTS); 227 final File myAlbumScreenshotsImg = assertCreateNewImage(myAlbumScreenshotsDir); 228 229 try { 230 assertGetCategoriesMatchMultiple(Arrays.asList( 231 Pair.create(ALBUM_ID_SCREENSHOTS, 3), 232 Pair.create(ALBUM_ID_DOWNLOADS, 1) 233 )); 234 } finally { 235 imageFile.delete(); 236 imageFileInScreenshotDirInDownloads.delete(); 237 myAlbumScreenshotsImg.delete(); 238 myAlbumScreenshotsDir.delete(); 239 deleteTopLevelScreenshotDir(); 240 } 241 } 242 243 /** 244 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 245 * about {@link #ALBUM_ID_SCREENSHOTS}. 246 */ 247 @Test testGetCategories_not_screenshots()248 public void testGetCategories_not_screenshots() throws Exception { 249 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 250 assertThat(c.getCount()).isEqualTo(0); 251 252 // negative test case: image file which should not be returned in Screenshots category 253 final File cameraDir = getCameraDir(); 254 File imageFile = assertCreateNewImage(cameraDir); 255 try { 256 assertGetCategoriesMatchSingle(ALBUM_ID_SCREENSHOTS, /* numberOfItems */ 0); 257 } finally { 258 imageFile.delete(); 259 } 260 } 261 262 /** 263 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 264 * about {@link AlbumColumns#ALBUM_ID_FAVORITES}. 265 */ 266 @Test testGetCategories_favorites()267 public void testGetCategories_favorites() throws Exception { 268 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 269 assertThat(c.getCount()).isEqualTo(0); 270 271 // positive test case: image file which should be returned in favorites category 272 final File picturesDir = getPicturesDir(); 273 final File imageFile = assertCreateNewImage(picturesDir); 274 setIsFavorite(imageFile); 275 try { 276 assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */1); 277 } finally { 278 imageFile.delete(); 279 } 280 } 281 282 /** 283 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 284 * about {@link AlbumColumns#ALBUM_ID_FAVORITES}. 285 */ 286 @Test testGetCategories_not_favorites()287 public void testGetCategories_not_favorites() throws Exception { 288 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 289 assertThat(c.getCount()).isEqualTo(0); 290 291 // negative test case: image file which should not be returned in favorites category 292 final File picturesDir = getPicturesDir(); 293 final File nonFavImageFile = assertCreateNewImage(picturesDir); 294 try { 295 assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */ 0); 296 } finally { 297 nonFavImageFile.delete(); 298 } 299 } 300 301 /** 302 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 303 * about {@link #ALBUM_ID_DOWNLOADS}. 304 */ 305 @Test testGetCategories_downloads()306 public void testGetCategories_downloads() throws Exception { 307 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 308 assertThat(c.getCount()).isEqualTo(0); 309 310 // Create 1 image file in Downloads dir to test 311 final File downloadsDir = getDownloadsDir(); 312 final File imageFile = assertCreateNewImage(downloadsDir); 313 try { 314 assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 1); 315 } finally { 316 imageFile.delete(); 317 } 318 } 319 320 /** 321 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 322 * about {@link #ALBUM_ID_DOWNLOADS}. 323 */ 324 @Test testGetCategories_not_downloads()325 public void testGetCategories_not_downloads() throws Exception { 326 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 327 assertThat(c.getCount()).isEqualTo(0); 328 329 // negative test case: image file which should not be returned in Downloads category 330 final File picturesDir = getPicturesDir(); 331 final File nonDownloadsImageFile = assertCreateNewImage(picturesDir); 332 try { 333 assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 0); 334 } finally { 335 nonDownloadsImageFile.delete(); 336 } 337 } 338 339 /** 340 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 341 * about {@link #ALBUM_ID_VIDEOS}. 342 */ 343 @Test testGetCategories_camera_and_videos()344 public void testGetCategories_camera_and_videos() throws Exception { 345 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 346 assertThat(c.getCount()).isEqualTo(0); 347 348 // Create 1 video file in Camera dir to test 349 final File cameraDir = getCameraDir(); 350 File videoFile = assertCreateNewVideo(cameraDir); 351 try { 352 assertGetCategoriesMatchMultiple(Arrays.asList( 353 Pair.create(ALBUM_ID_VIDEOS, 1), 354 Pair.create(ALBUM_ID_CAMERA, 1) 355 )); 356 } finally { 357 videoFile.delete(); 358 } 359 } 360 361 /** 362 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 363 * about {@link AlbumColumns#ALBUM_ID_FAVORITES}. 364 */ 365 @Test testGetCategories_screenshots_and_favorites()366 public void testGetCategories_screenshots_and_favorites() throws Exception { 367 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 368 assertThat(c.getCount()).isEqualTo(0); 369 370 // Create 1 image file in Screenshots dir to test 371 final File screenshotsDir = getScreenshotsDir(); 372 File imageFile = assertCreateNewImage(screenshotsDir); 373 setIsFavorite(imageFile); 374 try { 375 assertGetCategoriesMatchMultiple(Arrays.asList( 376 Pair.create(ALBUM_ID_FAVORITES, 1), 377 Pair.create(ALBUM_ID_SCREENSHOTS, 1) 378 )); 379 } finally { 380 imageFile.delete(); 381 } 382 } 383 384 /** 385 * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info 386 * about {@link AlbumColumns#ALBUM_ID_DOWNLOADS} and {@link AlbumColumns#ALBUM_ID_FAVORITES}. 387 */ 388 @Test testGetCategories_downloads_and_favorites()389 public void testGetCategories_downloads_and_favorites() throws Exception { 390 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 391 assertThat(c.getCount()).isEqualTo(0); 392 393 // Create 1 image file in Screenshots dir to test 394 final File downloadsDir = getDownloadsDir(); 395 File imageFile = assertCreateNewImage(downloadsDir); 396 setIsFavorite(imageFile); 397 try { 398 assertGetCategoriesMatchMultiple(Arrays.asList( 399 Pair.create(ALBUM_ID_FAVORITES, 1), 400 Pair.create(ALBUM_ID_DOWNLOADS, 1) 401 )); 402 } finally { 403 imageFile.delete(); 404 } 405 } 406 407 /** 408 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all 409 * images and videos. 410 */ 411 @Test testGetItems()412 public void testGetItems() throws Exception { 413 // Create 1 image and 1 video file to test 414 // Both files should be returned. 415 File imageFile = assertCreateNewImage(); 416 File videoFile = assertCreateNewVideo(); 417 try { 418 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 419 /* mimeType */ null, /* userId */ null); 420 assertThat(res).isNotNull(); 421 assertThat(res.getCount()).isEqualTo(2); 422 423 assertThatOnlyImagesVideos(res); 424 // Reset the cursor back. Cursor#moveToPosition(-1) will reset the position to -1, 425 // but since there is no such valid cursor position, it returns false. 426 assertThat(res.moveToPosition(-1)).isFalse(); 427 assertThatAllImagesVideos(res.getCount()); 428 } finally { 429 imageFile.delete(); 430 videoFile.delete(); 431 } 432 } 433 434 @Test testGetItems_sortOrder()435 public void testGetItems_sortOrder() throws Exception { 436 try { 437 final long timeNow = System.nanoTime() / 1000; 438 final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri( 439 new File(getDownloadsDir(), "latest_" + IMAGE_FILE_NAME), timeNow + 1000); 440 final Uri imageFileDateNowUri 441 = prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow); 442 final Uri videoFileDateNowUri 443 = prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow); 444 445 // This is the list of uris based on the expected sort order of items returned by 446 // ItemsProvider#getAllItems 447 List<Uri> uris = new ArrayList<>(); 448 // This is the latest image file 449 uris.add(imageFileDateNowPlus1Uri); 450 // Video file was scanned after image file, hence has higher _id than image file 451 uris.add(videoFileDateNowUri); 452 uris.add(imageFileDateNowUri); 453 454 try (Cursor cursor = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 455 /* mimeType */ null, /* userId */ null)) { 456 assertThat(cursor).isNotNull(); 457 458 final int expectedCount = uris.size(); 459 assertThat(cursor.getCount()).isEqualTo(expectedCount); 460 461 int rowNum = 0; 462 assertThat(cursor.moveToFirst()).isTrue(); 463 final int idColumnIndex = cursor.getColumnIndexOrThrow(MediaColumns.ID); 464 while (rowNum < expectedCount) { 465 assertWithMessage("id at row:" + rowNum + " is expected to be" 466 + " same as id in " + uris.get(rowNum)) 467 .that(String.valueOf(cursor.getLong(idColumnIndex))) 468 .isEqualTo(uris.get(rowNum).getLastPathSegment()); 469 cursor.moveToNext(); 470 rowNum++; 471 } 472 } 473 } finally { 474 deleteAllFilesNoThrow(); 475 } 476 } 477 478 /** 479 * Tests {@link {@link ItemsProvider#getAllItems(Category, int, String[], UserId)}} does not 480 * return hidden images/videos. 481 */ 482 @Test testGetItems_nonMedia()483 public void testGetItems_nonMedia() throws Exception { 484 // Create 1 image and 1 video file in a hidden dir to test 485 // Both should not be returned. 486 File hiddenDir = createHiddenDir(); 487 File imageFileHidden = assertCreateNewImage(hiddenDir); 488 File videoFileHidden = assertCreateNewVideo(hiddenDir); 489 try { 490 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 491 /* mimeType */ null, /* userId */ null); 492 assertThat(res).isNotNull(); 493 assertThat(res.getCount()).isEqualTo(0); 494 } finally { 495 imageFileHidden.delete(); 496 videoFileHidden.delete(); 497 hiddenDir.delete(); 498 } 499 } 500 501 /** 502 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all 503 * images and videos based on the mimeType. Image mimeType should only return images. 504 */ 505 @Test testGetItemsImages()506 public void testGetItemsImages() throws Exception { 507 // Create 1 image and 1 video file to test 508 // Only 1 should be returned. 509 File imageFile = assertCreateNewImage(); 510 File videoFile = assertCreateNewVideo(); 511 try { 512 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 513 /* mimeType */ new String[]{ "image/*"}, /* userId */ null); 514 assertThat(res).isNotNull(); 515 assertThat(res.getCount()).isEqualTo(1); 516 517 assertThatOnlyImages(res); 518 assertThatAllImages(res.getCount()); 519 } finally { 520 imageFile.delete(); 521 videoFile.delete(); 522 } 523 } 524 525 /** 526 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all 527 * images and videos based on the mimeType. Image mimeType should only return images. 528 */ 529 @Test testGetItemsImages_png()530 public void testGetItemsImages_png() throws Exception { 531 // Create a jpg file image. Tests negative use case, this should not be returned below. 532 File imageFile = assertCreateNewImage(); 533 try { 534 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 535 /* mimeType */ new String[]{"image/png"}, /* userId */ null); 536 assertThat(res).isNotNull(); 537 assertThat(res.getCount()).isEqualTo(0); 538 } finally { 539 imageFile.delete(); 540 } 541 } 542 543 /** 544 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return 545 * hidden images/videos. 546 */ 547 @Test testGetItemsImages_nonMedia()548 public void testGetItemsImages_nonMedia() throws Exception { 549 // Create 1 image and 1 video file in a hidden dir to test 550 // Both should not be returned. 551 File hiddenDir = createHiddenDir(); 552 File imageFileHidden = assertCreateNewImage(hiddenDir); 553 File videoFileHidden = assertCreateNewVideo(hiddenDir); 554 try { 555 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 556 /* mimeType */ new String[]{"image/*"}, /* userId */ null); 557 assertThat(res).isNotNull(); 558 assertThat(res.getCount()).isEqualTo(0); 559 } finally { 560 imageFileHidden.delete(); 561 videoFileHidden.delete(); 562 hiddenDir.delete(); 563 } 564 } 565 566 /** 567 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all 568 * images and videos based on the mimeType. Video mimeType should only return videos. 569 */ 570 @Test testGetItemsVideos()571 public void testGetItemsVideos() throws Exception { 572 // Create 1 image and 1 video file to test 573 // Only 1 should be returned. 574 File imageFile = assertCreateNewImage(); 575 File videoFile = assertCreateNewVideo(); 576 try { 577 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 578 /* mimeType */ new String[]{"video/*"}, /* userId */ null); 579 assertThat(res).isNotNull(); 580 assertThat(res.getCount()).isEqualTo(1); 581 582 assertThatOnlyVideos(res); 583 assertThatAllVideos(res.getCount()); 584 } finally { 585 imageFile.delete(); 586 videoFile.delete(); 587 } 588 } 589 590 /** 591 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all 592 * images and videos based on the mimeType. Image mimeType should only return images. 593 */ 594 @Test testGetItemsVideos_mp4()595 public void testGetItemsVideos_mp4() throws Exception { 596 // Create a mp4 video file. Tests positive use case, this should be returned below. 597 File videoFile = assertCreateNewVideo(); 598 try { 599 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 600 /* mimeType */ new String[]{"video/mp4"}, /* userId */ null); 601 assertThat(res).isNotNull(); 602 assertThat(res.getCount()).isEqualTo(1); 603 } finally { 604 videoFile.delete(); 605 } 606 } 607 608 /** 609 * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return 610 * hidden images/videos. 611 */ 612 @Test testGetItemsVideos_nonMedia()613 public void testGetItemsVideos_nonMedia() throws Exception { 614 // Create 1 image and 1 video file in a hidden dir to test the API. 615 // Both should not be returned. 616 File hiddenDir = createHiddenDir(); 617 File imageFileHidden = assertCreateNewImage(hiddenDir); 618 File videoFileHidden = assertCreateNewVideo(hiddenDir); 619 try { 620 final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1, 621 /* mimeType */ new String[]{"video/*"}, /* userId */ null); 622 623 assertThat(res).isNotNull(); 624 assertThat(res.getCount()).isEqualTo(0); 625 } finally { 626 imageFileHidden.delete(); 627 videoFileHidden.delete(); 628 hiddenDir.delete(); 629 } 630 } 631 632 /** 633 * Tests {@link ItemsProvider#getLocalItems(Category, int, String[], UserId)} to returns only 634 * local content. 635 */ 636 @Test testGetLocalItems_withCloud()637 public void testGetLocalItems_withCloud() throws Exception { 638 File videoFile = assertCreateNewVideo(); 639 try { 640 mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages( 641 sTargetPackageName); 642 // Init cloud provider and add one item 643 setupCloudProvider((cloudMediaGenerator) -> { 644 cloudMediaGenerator.addMedia(null, "cloud_id1"); 645 }); 646 647 // Verify that getLocalItems includes only local contents 648 try (Cursor c = mItemsProvider.getLocalItems(Category.DEFAULT, -1, new String[]{}, 649 UserId.CURRENT_USER)) { 650 assertThat(c.getCount()).isEqualTo(1); 651 652 assertThat(c.moveToFirst()).isTrue(); 653 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 654 .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY); 655 } 656 657 // Verify that getAllItems includes cloud items 658 try (Cursor c = mItemsProvider.getAllItems(Category.DEFAULT, -1, new String[]{}, 659 UserId.CURRENT_USER)) { 660 assertThat(c.getCount()).isEqualTo(2); 661 662 // Verify that the first item is cloud item 663 assertThat(c.moveToFirst()).isTrue(); 664 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 665 .isEqualTo(CloudProviderPrimary.AUTHORITY); 666 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID))).isEqualTo( 667 "cloud_id1"); 668 669 // Verify that the second item is local item 670 assertThat(c.moveToNext()).isTrue(); 671 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 672 .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY); 673 } 674 } finally { 675 videoFile.delete(); 676 setCloudProvider(null); 677 mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature(); 678 } 679 } 680 681 @Test testGetLocalItems_mergedAlbum_withCloud()682 public void testGetLocalItems_mergedAlbum_withCloud() throws Exception { 683 File videoFile = assertCreateNewVideo(); 684 Category videoAlbum = new Category(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS, 685 LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 10, true); 686 try { 687 mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages( 688 sTargetPackageName); 689 // Init cloud provider and add one item 690 setupCloudProvider((cloudMediaGenerator) -> { 691 cloudMediaGenerator.addMedia(null, "cloud_id1", null, "video/mp4", 0, 1024, false); 692 }); 693 694 // Verify that getLocalItems for merged album "Video" includes only local contents 695 try (Cursor c = mItemsProvider.getLocalItems(videoAlbum, -1, new String[]{}, 696 UserId.CURRENT_USER)) { 697 assertThat(c.getCount()).isEqualTo(1); 698 assertThat(c.moveToFirst()).isTrue(); 699 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 700 .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY); 701 } 702 703 // Verify that getAllItems for merged album "Video" also includes cloud contents 704 try (Cursor c = mItemsProvider.getAllItems(videoAlbum, -1, new String[]{}, 705 UserId.CURRENT_USER)) { 706 assertThat(c.getCount()).isEqualTo(2); 707 // Verify that the first item is cloud item 708 assertThat(c.moveToFirst()).isTrue(); 709 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 710 .isEqualTo(CloudProviderPrimary.AUTHORITY); 711 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID))) 712 .isEqualTo("cloud_id1"); 713 714 // Verify that the second item is local item 715 assertThat(c.moveToNext()).isTrue(); 716 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY))) 717 .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY); 718 } 719 } finally { 720 videoFile.delete(); 721 setCloudProvider(null); 722 mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature(); 723 } 724 } 725 726 @Test testGetLocalCategories_withCloud()727 public void testGetLocalCategories_withCloud() throws Exception { 728 File videoFile = assertCreateNewVideo(getMoviesDir()); 729 File screenshotFile = assertCreateNewImage(getScreenshotsDir()); 730 final String cloudAlbum = "testAlbum"; 731 732 try { 733 mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages( 734 sTargetPackageName); 735 // Init cloud provider with 2 items and one cloud album 736 setupCloudProvider((cloudMediaGenerator) -> { 737 cloudMediaGenerator.addMedia(null, "cloud_id1", null, "video/mp4", 0, 1024, false); 738 cloudMediaGenerator.addMedia(null, "cloud_id2", cloudAlbum, "image/jpeg", 0, 1024, 739 false); 740 cloudMediaGenerator.createAlbum(cloudAlbum); 741 }); 742 743 // Verify that getLocalCategories only returns local albums 744 try (Cursor c = mItemsProvider.getLocalCategories(/* mimeType */ null, 745 /* userId */ null)) { 746 assertGetCategoriesMatchMultiple(c, Arrays.asList( 747 Pair.create(ALBUM_ID_VIDEOS, 1), 748 Pair.create(ALBUM_ID_SCREENSHOTS, 1) 749 )); 750 } 751 752 753 // Verify that getAllCategories returns local + cloud albums 754 try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, 755 /* userId */ null)) { 756 assertGetCategoriesMatchMultiple(c, Arrays.asList( 757 Pair.create(ALBUM_ID_VIDEOS, 2), 758 Pair.create(ALBUM_ID_SCREENSHOTS, 1), 759 Pair.create(cloudAlbum, 1) 760 )); 761 } 762 } finally { 763 videoFile.delete(); 764 screenshotFile.delete(); 765 setCloudProvider(null); 766 mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature(); 767 } 768 } 769 setupCloudProvider(Consumer<MediaGenerator> initMedia)770 private void setupCloudProvider(Consumer<MediaGenerator> initMedia) { 771 MediaGenerator cloudPrimaryMediaGenerator = 772 PickerProviderMediaGenerator.getMediaGenerator(CloudProviderPrimary.AUTHORITY); 773 cloudPrimaryMediaGenerator.resetAll(); 774 cloudPrimaryMediaGenerator.setMediaCollectionId("COLLECTION_1"); 775 initMedia.accept(cloudPrimaryMediaGenerator); 776 777 // Set the newly initialized cloud provider. 778 setCloudProvider(CloudProviderPrimary.AUTHORITY); 779 780 mIsolatedResolver.call(MediaStore.AUTHORITY, MediaStore.SYNC_PROVIDERS_CALL, null, 781 Bundle.EMPTY); 782 } 783 setCloudProvider(String authority)784 private void setCloudProvider(String authority) { 785 Bundle in = new Bundle(); 786 in.putString(MediaStore.EXTRA_CLOUD_PROVIDER, authority); 787 mIsolatedResolver.call(MediaStore.AUTHORITY, MediaStore.SET_CLOUD_PROVIDER_CALL, null, in); 788 if (authority != null) { 789 assertWithMessage("Expected " + authority + " to be current cloud provider.") 790 .that(MediaStore.isCurrentCloudMediaProviderAuthority(mIsolatedResolver, 791 authority)).isTrue(); 792 } 793 } 794 assertGetCategoriesMatchSingle(String expectedCategoryName, int expectedNumberOfItems)795 private void assertGetCategoriesMatchSingle(String expectedCategoryName, 796 int expectedNumberOfItems) throws Exception { 797 if (expectedNumberOfItems == 0) { 798 assertCategoriesNoMatch(expectedCategoryName); 799 return; 800 } 801 802 Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null); 803 assertThat(c).isNotNull(); 804 assertThat(c.getCount()).isEqualTo(1); 805 806 // Assert that only expected category is returned and has expectedNumberOfItems items in it 807 assertThat(c.moveToFirst()).isTrue(); 808 final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME); 809 final int numOfItemsColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COUNT); 810 final int coverIdIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COVER_ID); 811 812 final String categoryName = c.getString(nameColumnIndex); 813 final int numOfItems = c.getInt(numOfItemsColumnIndex); 814 final Uri coverUri = ItemsProvider.getItemsUri(c.getString(coverIdIndex), 815 LOCAL_PICKER_PROVIDER_AUTHORITY, UserId.CURRENT_USER); 816 817 assertThat(categoryName).isEqualTo(expectedCategoryName); 818 assertThat(numOfItems).isEqualTo(expectedNumberOfItems); 819 assertCategoryUriIsValid(coverUri); 820 } 821 assertCategoryUriIsValid(Uri uri)822 private void assertCategoryUriIsValid(Uri uri) throws Exception { 823 try (AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*", 824 null, null)) { 825 assertThat(fd1).isNotNull(); 826 } 827 try (ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r")) { 828 assertThat(fd2).isNotNull(); 829 } 830 } 831 assertCategoriesNoMatch(String expectedCategoryName)832 private void assertCategoriesNoMatch(String expectedCategoryName) { 833 try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) { 834 while (c != null && c.moveToNext()) { 835 final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME); 836 final String categoryName = c.getString(nameColumnIndex); 837 assertThat(categoryName).isNotEqualTo(expectedCategoryName); 838 } 839 } 840 } 841 assertGetCategoriesMatchMultiple(List<Pair<String, Integer>> categories)842 private void assertGetCategoriesMatchMultiple(List<Pair<String, Integer>> categories) { 843 try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) { 844 assertGetCategoriesMatchMultiple(c, categories); 845 } 846 } 847 assertGetCategoriesMatchMultiple(Cursor c, List<Pair<String, Integer>> categories)848 private void assertGetCategoriesMatchMultiple(Cursor c, 849 List<Pair<String, Integer>> categories) { 850 assertThat(c).isNotNull(); 851 assertWithMessage("Expected number of albums") 852 .that(c.getCount()).isEqualTo(categories.size()); 853 854 final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME); 855 final int numOfItemsColumnIndex = c.getColumnIndexOrThrow( 856 AlbumColumns.MEDIA_COUNT); 857 for (Pair<String, Integer> category : categories) { 858 c.moveToNext(); 859 860 assertThat(category.first).isEqualTo(c.getString(nameColumnIndex)); 861 assertWithMessage("Expected item count for " + category.first).that( 862 c.getInt(numOfItemsColumnIndex)).isEqualTo(category.second); 863 } 864 } 865 setIsFavorite(File file)866 private void setIsFavorite(File file) { 867 final Uri uri = MediaStore.scanFile(mIsolatedResolver, file); 868 final ContentValues values = new ContentValues(); 869 values.put(MediaStore.MediaColumns.IS_FAVORITE, 1); 870 // Assert that 1 row corresponding to this file is updated. 871 assertThat(mIsolatedResolver.update(uri, values, null)).isEqualTo(1); 872 // Wait for MediaStore to be Idle to reduce flakes caused by database updates 873 MediaStore.waitForIdle(mIsolatedResolver); 874 } 875 assertThatOnlyImagesVideos(Cursor c)876 private void assertThatOnlyImagesVideos(Cursor c) throws Exception { 877 while (c.moveToNext()) { 878 int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); 879 String mimeType = c.getString(mimeTypeColumn); 880 assertThat(isImageMimeType(mimeType) || isVideoMimeType(mimeType)).isTrue(); 881 } 882 } 883 assertThatOnlyImages(Cursor c)884 private void assertThatOnlyImages(Cursor c) throws Exception { 885 while (c.moveToNext()) { 886 int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); 887 String mimeType = c.getString(mimeTypeColumn); 888 assertThat(isImageMimeType(mimeType)).isTrue(); 889 } 890 } 891 assertThatOnlyVideos(Cursor c)892 private void assertThatOnlyVideos(Cursor c) throws Exception { 893 while (c.moveToNext()) { 894 int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); 895 String mimeType = c.getString(mimeTypeColumn); 896 assertThat(isVideoMimeType(mimeType)).isTrue(); 897 } 898 } 899 assertThatAllImagesVideos(int count)900 private void assertThatAllImagesVideos(int count) { 901 int countOfImages = getCountOfMediaStoreImages(); 902 int countOfVideos = getCountOfMediaStoreVideos(); 903 assertThat(count).isEqualTo(countOfImages + countOfVideos); 904 } 905 assertThatAllImages(int count)906 private void assertThatAllImages(int count) { 907 int countOfImages = getCountOfMediaStoreImages(); 908 assertThat(count).isEqualTo(countOfImages); 909 } 910 assertThatAllVideos(int count)911 private void assertThatAllVideos(int count) { 912 int countOfVideos = getCountOfMediaStoreVideos(); 913 assertThat(count).isEqualTo(countOfVideos); 914 } 915 createTestScreenshotImages()916 private void createTestScreenshotImages() throws IOException { 917 // Top Level /Screenshots/ directory is not allowed by MediaProvider, so the directory 918 // and test files in it are created via shell commands. 919 920 final String createTopLevelScreenshotDirCommand = 921 "mkdir -p " 922 + Environment.getExternalStorageDirectory().getPath() 923 + "/" 924 + Environment.DIRECTORY_SCREENSHOTS; 925 final String createTopLevelScreenshotImgCommand = 926 "touch " + getTopLevelScreenshotsDir().getPath() + "/" + IMAGE_FILE_NAME; 927 final String writeDataTopLevelScreenshotImgCommand = 928 "echo 1 > " + getTopLevelScreenshotsDir().getPath() + "/" + IMAGE_FILE_NAME; 929 930 executeShellCommand(createTopLevelScreenshotDirCommand); 931 executeShellCommand(createTopLevelScreenshotImgCommand); 932 // Writes 1 byte to the file. 933 executeShellCommand(writeDataTopLevelScreenshotImgCommand); 934 935 final File topLevelScreenshotsDirImage = 936 new File(getTopLevelScreenshotsDir(), IMAGE_FILE_NAME); 937 938 // Force the mock MediaProvider to scan. 939 final Uri uri = MediaStore.scanFile(mIsolatedResolver, topLevelScreenshotsDirImage); 940 assertWithMessage("Uri obtained by scanning file " + topLevelScreenshotsDirImage) 941 .that(uri) 942 .isNotNull(); 943 // Wait for picker db sync 944 MediaStore.waitForIdle(mIsolatedResolver); 945 } 946 getCountOfMediaStoreImages()947 private int getCountOfMediaStoreImages() { 948 try (Cursor c = mIsolatedResolver.query( 949 MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) { 950 assertThat(c.moveToFirst()).isTrue(); 951 return c.getCount(); 952 } 953 } 954 getCountOfMediaStoreVideos()955 private int getCountOfMediaStoreVideos() { 956 try (Cursor c = mIsolatedResolver.query( 957 MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) { 958 assertThat(c.moveToFirst()).isTrue(); 959 return c.getCount(); 960 } 961 } 962 assertCreateNewVideo(File dir)963 private File assertCreateNewVideo(File dir) throws Exception { 964 return assertCreateNewFile(dir, VIDEO_FILE_NAME); 965 } 966 assertCreateNewImage(File dir)967 private File assertCreateNewImage(File dir) throws Exception { 968 return assertCreateNewFile(dir, IMAGE_FILE_NAME); 969 } 970 assertCreateNewVideo()971 private File assertCreateNewVideo() throws Exception { 972 return assertCreateNewFile(getDownloadsDir(), VIDEO_FILE_NAME); 973 } 974 assertCreateNewImage()975 private File assertCreateNewImage() throws Exception { 976 return assertCreateNewFile(getDownloadsDir(), IMAGE_FILE_NAME); 977 } 978 assertCreateNewFile(File parentDir, String fileName)979 private File assertCreateNewFile(File parentDir, String fileName) throws Exception { 980 final File file = new File(parentDir, fileName); 981 prepareFileAndGetUri(file, /* lastModifiedTime */ -1); 982 983 return file; 984 } 985 prepareFileAndGetUri(File file, long lastModifiedTime)986 private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException { 987 ensureParentExists(file.getParentFile()); 988 989 assertThat(file.createNewFile()).isTrue(); 990 991 // Write 1 byte because 0byte files are not valid in the picker db 992 try (FileOutputStream fos = new FileOutputStream(file)) { 993 fos.write(1); 994 } 995 996 if (lastModifiedTime != -1) { 997 file.setLastModified(lastModifiedTime); 998 } 999 1000 final Uri uri = MediaStore.scanFile(mIsolatedResolver, file); 1001 assertWithMessage("Uri obtained by scanning file " + file) 1002 .that(uri).isNotNull(); 1003 // Wait for picker db sync 1004 MediaStore.waitForIdle(mIsolatedResolver); 1005 1006 return uri; 1007 } 1008 ensureParentExists(File parent)1009 private void ensureParentExists(File parent) { 1010 if (!parent.exists()) { 1011 parent.mkdirs(); 1012 } 1013 assertThat(parent.exists()).isTrue(); 1014 } 1015 getDownloadsDir()1016 private File getDownloadsDir() { 1017 return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); 1018 } 1019 getDcimDir()1020 private File getDcimDir() { 1021 return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DCIM); 1022 } 1023 getPicturesDir()1024 private File getPicturesDir() { 1025 return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES); 1026 } 1027 getMoviesDir()1028 private File getMoviesDir() { 1029 return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_MOVIES); 1030 } 1031 getCameraDir()1032 private File getCameraDir() { 1033 return new File(getDcimDir(), "Camera"); 1034 } 1035 getScreenshotsDir()1036 private File getScreenshotsDir() { 1037 return new File(getPicturesDir(), Environment.DIRECTORY_SCREENSHOTS); 1038 } 1039 getTopLevelScreenshotsDir()1040 private File getTopLevelScreenshotsDir() { 1041 return new File( 1042 Environment.getExternalStorageDirectory(), Environment.DIRECTORY_SCREENSHOTS); 1043 } 1044 getScreenshotsDirFromDownloadsDir()1045 private File getScreenshotsDirFromDownloadsDir() { 1046 return new File(getDownloadsDir(), Environment.DIRECTORY_SCREENSHOTS); 1047 } 1048 createHiddenDir()1049 private File createHiddenDir() throws Exception { 1050 File parentDir = new File(Environment.getExternalStorageDirectory(), 1051 Environment.DIRECTORY_DOWNLOADS); 1052 File dir = new File(parentDir, HIDDEN_DIR_NAME); 1053 dir.mkdirs(); 1054 File nomedia = new File(dir, ".nomedia"); 1055 nomedia.createNewFile(); 1056 1057 MediaStore.scanFile(mIsolatedResolver, nomedia); 1058 1059 return dir; 1060 } 1061 deleteAllFilesNoThrow()1062 private void deleteAllFilesNoThrow() { 1063 try (Cursor c = mIsolatedResolver.query( 1064 MediaStore.Files.getContentUri(VOLUME_EXTERNAL), 1065 new String[] {MediaStore.MediaColumns.DATA}, null, null)) { 1066 while(c.moveToNext()) { 1067 (new File(c.getString( 1068 c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)))).delete(); 1069 } 1070 } 1071 } 1072 deleteTopLevelScreenshotDir()1073 private void deleteTopLevelScreenshotDir() throws IOException { 1074 final String removeTopLevelScreenshotDirCommand = 1075 "rm -rf " + getTopLevelScreenshotsDir().getPath(); 1076 executeShellCommand(removeTopLevelScreenshotDirCommand); 1077 } 1078 1079 /** Executes a shell command. */ executeShellCommand(String command)1080 private static String executeShellCommand(String command) throws IOException { 1081 int attempt = 0; 1082 while (attempt++ < 5) { 1083 try { 1084 return executeShellCommandInternal(command); 1085 } catch (InterruptedIOException e) { 1086 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 1087 } 1088 } 1089 throw new IOException("Failed to execute " + command); 1090 } 1091 executeShellCommandInternal(String cmd)1092 private static String executeShellCommandInternal(String cmd) throws IOException { 1093 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 1094 try (FileInputStream output = new FileInputStream( 1095 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 1096 return new String(ByteStreams.toByteArray(output)); 1097 } 1098 } 1099 } 1100