1 /* 2 * Copyright (C) 2019 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.client; 18 19 import static android.provider.MediaStore.rewriteToLegacy; 20 21 import static com.google.common.truth.Truth.assertWithMessage; 22 23 import static org.junit.Assert.assertNotNull; 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertFalse; 26 import static org.junit.Assert.assertTrue; 27 import static org.junit.Assert.fail; 28 29 import android.app.UiAutomation; 30 import android.content.ContentProviderClient; 31 import android.content.ContentProviderOperation; 32 import android.content.ContentResolver; 33 import android.content.ContentUris; 34 import android.content.ContentValues; 35 import android.content.Context; 36 import android.content.pm.ProviderInfo; 37 import android.database.Cursor; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.Environment; 41 import android.os.FileUtils; 42 import android.os.ParcelFileDescriptor; 43 import android.os.SystemClock; 44 import android.os.storage.StorageManager; 45 import android.provider.BaseColumns; 46 import android.provider.MediaStore; 47 import android.provider.MediaStore.Audio.AudioColumns; 48 import android.provider.MediaStore.DownloadColumns; 49 import android.provider.MediaStore.Files.FileColumns; 50 import android.provider.MediaStore.Images.ImageColumns; 51 import android.provider.MediaStore.MediaColumns; 52 import android.provider.MediaStore.Video.VideoColumns; 53 import android.system.ErrnoException; 54 import android.system.Os; 55 import android.util.Log; 56 import android.webkit.MimeTypeMap; 57 58 import androidx.annotation.NonNull; 59 import androidx.test.filters.FlakyTest; 60 import androidx.test.InstrumentationRegistry; 61 import androidx.test.runner.AndroidJUnit4; 62 63 import com.google.common.truth.Truth; 64 65 import org.junit.Assume; 66 import org.junit.Before; 67 import org.junit.Test; 68 import org.junit.runner.RunWith; 69 70 import java.io.BufferedReader; 71 import java.io.File; 72 import java.io.FileInputStream; 73 import java.io.FileOutputStream; 74 import java.io.IOException; 75 import java.io.InputStream; 76 import java.io.InputStreamReader; 77 import java.io.InterruptedIOException; 78 import java.io.OutputStream; 79 import java.nio.charset.StandardCharsets; 80 import java.util.ArrayList; 81 import java.util.concurrent.TimeUnit; 82 83 /** 84 * Verify that we preserve information from the old "legacy" provider from 85 * before we migrated into a Mainline module. 86 * <p> 87 * Specifically, values like {@link BaseColumns#_ID} and user edits like 88 * {@link MediaColumns#IS_FAVORITE} should be retained. 89 */ 90 @RunWith(AndroidJUnit4.class) 91 @FlakyTest(bugId = 176977253) 92 public class LegacyProviderMigrationTest { 93 private static final String TAG = "LegacyTest"; 94 95 // TODO: expand test to cover secondary storage devices 96 private String mVolumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; 97 98 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10); 99 private static final long POLLING_SLEEP_MILLIS = 100; 100 101 /** 102 * Number of media items to insert for {@link #testLegacy_Extreme()}. 103 */ 104 private static final int EXTREME_COUNT = 10_000; 105 106 private Uri mExternalAudio; 107 private Uri mExternalVideo; 108 private Uri mExternalImages; 109 private Uri mExternalDownloads; 110 private Uri mExternalPlaylists; 111 112 @Before setUp()113 public void setUp() throws Exception { 114 Log.d(TAG, "Using volume " + mVolumeName); 115 mExternalAudio = MediaStore.Audio.Media.getContentUri(mVolumeName); 116 mExternalVideo = MediaStore.Video.Media.getContentUri(mVolumeName); 117 mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName); 118 mExternalDownloads = MediaStore.Downloads.getContentUri(mVolumeName); 119 120 Uri playlists = MediaStore.Audio.Playlists.getContentUri(mVolumeName); 121 mExternalPlaylists = playlists.buildUpon() 122 .appendQueryParameter("silent", "true").build(); 123 } 124 generateValues(int mediaType, String mimeType, String dirName)125 private ContentValues generateValues(int mediaType, String mimeType, String dirName) 126 throws Exception { 127 return generateValues(mediaType, mimeType, dirName, 0); 128 } 129 generateValues(int mediaType, String mimeType, String dirName, int resId)130 private ContentValues generateValues(int mediaType, String mimeType, String dirName, int resId) 131 throws Exception { 132 final Context context = InstrumentationRegistry.getContext(); 133 134 final File dir = context.getSystemService(StorageManager.class) 135 .getStorageVolume(MediaStore.Files.getContentUri(mVolumeName)).getDirectory(); 136 final File subDir = new File(dir, dirName); 137 File file = new File(subDir, "legacy" + System.nanoTime() + "." 138 + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)); 139 140 if (resId != 0) { 141 file = stageFile(resId, file.getAbsolutePath()); 142 } 143 144 final ContentValues values = new ContentValues(); 145 values.put(FileColumns.MEDIA_TYPE, mediaType); 146 values.put(MediaColumns.DATA, file.getAbsolutePath()); 147 values.put(MediaColumns.DISPLAY_NAME, file.getName()); 148 values.put(MediaColumns.MIME_TYPE, mimeType); 149 values.put(MediaColumns.VOLUME_NAME, mVolumeName); 150 values.put(MediaColumns.DATE_ADDED, String.valueOf(System.currentTimeMillis() / 1_000)); 151 values.put(MediaColumns.OWNER_PACKAGE_NAME, 152 InstrumentationRegistry.getContext().getPackageName()); 153 return values; 154 } 155 stageFile(int resId, String path)156 private static File stageFile(int resId, String path) throws Exception { 157 final Context context = InstrumentationRegistry.getContext(); 158 final File file = new File(path); 159 try (InputStream in = context.getResources().openRawResource(resId); 160 OutputStream out = new FileOutputStream(file)) { 161 FileUtils.copy(in, out); 162 } 163 return file; 164 } 165 166 @Test testLegacy_Orientation()167 public void testLegacy_Orientation() throws Exception { 168 // Use an image file with orientation of 90 degrees 169 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 170 "image/jpeg", Environment.DIRECTORY_PICTURES, R.raw.orientation_90); 171 values.put(MediaColumns.ORIENTATION, String.valueOf(90)); 172 doLegacy(mExternalImages, values); 173 } 174 175 @Test testLegacy_Pending()176 public void testLegacy_Pending() throws Exception { 177 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 178 "image/png", Environment.DIRECTORY_PICTURES); 179 values.put(MediaColumns.IS_PENDING, String.valueOf(1)); 180 values.put(MediaColumns.DATE_EXPIRES, String.valueOf(System.currentTimeMillis() / 1_000)); 181 doLegacy(mExternalImages, values); 182 } 183 184 @Test testLegacy_Trashed()185 public void testLegacy_Trashed() throws Exception { 186 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 187 "image/png", Environment.DIRECTORY_PICTURES); 188 values.put(MediaColumns.IS_TRASHED, String.valueOf(1)); 189 doLegacy(mExternalImages, values); 190 } 191 192 @Test testLegacy_Favorite()193 public void testLegacy_Favorite() throws Exception { 194 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 195 "image/png", Environment.DIRECTORY_PICTURES); 196 values.put(MediaColumns.IS_FAVORITE, String.valueOf(1)); 197 doLegacy(mExternalImages, values); 198 } 199 200 @Test testLegacy_Orphaned()201 public void testLegacy_Orphaned() throws Exception { 202 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 203 "image/png", Environment.DIRECTORY_PICTURES); 204 values.putNull(MediaColumns.OWNER_PACKAGE_NAME); 205 doLegacy(mExternalImages, values); 206 } 207 208 @Test testLegacy_Audio()209 public void testLegacy_Audio() throws Exception { 210 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_AUDIO, 211 "audio/mpeg", Environment.DIRECTORY_MUSIC); 212 values.put(AudioColumns.BOOKMARK, String.valueOf(42)); 213 doLegacy(mExternalAudio, values); 214 } 215 216 @Test testLegacy_Video()217 public void testLegacy_Video() throws Exception { 218 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_VIDEO, 219 "video/mpeg", Environment.DIRECTORY_MOVIES); 220 values.put(VideoColumns.BOOKMARK, String.valueOf(42)); 221 values.put(VideoColumns.TAGS, "My Tags"); 222 values.put(VideoColumns.CATEGORY, "My Category"); 223 values.put(VideoColumns.IS_PRIVATE, String.valueOf(1)); 224 doLegacy(mExternalVideo, values); 225 } 226 227 @Test testLegacy_Image()228 public void testLegacy_Image() throws Exception { 229 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 230 "image/png", Environment.DIRECTORY_PICTURES); 231 values.put(ImageColumns.IS_PRIVATE, String.valueOf(1)); 232 doLegacy(mExternalImages, values); 233 } 234 235 @Test testLegacy_Download()236 public void testLegacy_Download() throws Exception { 237 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_NONE, 238 "application/x-iso9660-image", Environment.DIRECTORY_DOWNLOADS); 239 values.put(DownloadColumns.DOWNLOAD_URI, "http://example.com/download"); 240 values.put(DownloadColumns.REFERER_URI, "http://example.com/referer"); 241 doLegacy(mExternalDownloads, values); 242 } 243 244 /** 245 * Test that migrating from legacy database with volume_name=NULL doesn't 246 * result in empty cursor when queried. 247 */ 248 @Test testMigrateNullVolumeName()249 public void testMigrateNullVolumeName() throws Exception { 250 final ContentValues values = generateValues(FileColumns.MEDIA_TYPE_IMAGE, 251 "image/png", Environment.DIRECTORY_PICTURES); 252 values.remove(MediaColumns.VOLUME_NAME); 253 doLegacy(mExternalImages, values); 254 } 255 256 @Test testLegacy_PlaylistMap()257 public void testLegacy_PlaylistMap() throws Exception { 258 final Context context = InstrumentationRegistry.getTargetContext(); 259 final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 260 261 final ContentValues audios[] = new ContentValues[] { 262 generateValues(FileColumns.MEDIA_TYPE_AUDIO, "audio/mpeg", 263 Environment.DIRECTORY_MUSIC), 264 generateValues(FileColumns.MEDIA_TYPE_AUDIO, "audio/mpeg", 265 Environment.DIRECTORY_MUSIC), 266 }; 267 268 final String playlistMimeType = "audio/mpegurl"; 269 final ContentValues playlist = generateValues(FileColumns.MEDIA_TYPE_PLAYLIST, 270 playlistMimeType, "Playlists"); 271 final String playlistName = "LegacyPlaylistName_" + System.nanoTime(); 272 playlist.put(MediaStore.Audio.PlaylistsColumns.NAME, playlistName); 273 File playlistFile = new File(playlist.getAsString(MediaColumns.DATA)); 274 275 playlistFile.delete(); 276 277 final ContentValues playlistMap = new ContentValues(); 278 playlistMap.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, 1); 279 280 prepareProviders(context, ui); 281 282 try (ContentProviderClient legacy = context.getContentResolver() 283 .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) { 284 285 // Step 1: Insert the playlist entry into the playlists table. 286 final Uri playlistUri = rewriteToLegacy(legacy.insert( 287 rewriteToLegacy(mExternalPlaylists), playlist)); 288 long playlistId = ContentUris.parseId(playlistUri); 289 final Uri playlistMemberUri = MediaStore.rewriteToLegacy( 290 MediaStore.Audio.Playlists.Members.getContentUri(mVolumeName, playlistId) 291 .buildUpon() 292 .appendQueryParameter("silent", "true").build()); 293 294 295 for (ContentValues values : audios) { 296 // Step 2: Write the audio file to the legacy mediastore. 297 final Uri audioUri = 298 rewriteToLegacy(legacy.insert(rewriteToLegacy(mExternalAudio), values)); 299 // Remember our ID to check it later 300 values.put(MediaColumns._ID, audioUri.getLastPathSegment()); 301 302 303 long audioId = ContentUris.parseId(audioUri); 304 playlistMap.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 305 playlistMap.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 306 307 // Step 3: Add a mapping to playlist members. 308 legacy.insert(playlistMemberUri, playlistMap); 309 } 310 311 // Insert a stale row, We only have 3 items in the database. #4 is a stale row 312 // and will be skipped from the playlist during the migration. 313 playlistMap.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, 4); 314 legacy.insert(playlistMemberUri, playlistMap); 315 316 } 317 318 // This will delete MediaProvider data and restarts MediaProvider, and mounts storage. 319 clearProviders(context, ui); 320 321 // Verify scan on DEMAND doesn't delete any virtual playlist files. 322 MediaStore.scanFile(context.getContentResolver(), 323 Environment.getExternalStorageDirectory()); 324 325 // Playlist files are created from playlist NAME 326 final File musicDir = new File(context.getSystemService(StorageManager.class) 327 .getStorageVolume(MediaStore.Files.getContentUri(mVolumeName)).getDirectory(), 328 Environment.DIRECTORY_MUSIC); 329 playlistFile = new File(musicDir, playlistName + "." 330 + MimeTypeMap.getSingleton().getExtensionFromMimeType(playlistMimeType)); 331 // Wait for scan on MEDIA_MOUNTED to create "real" playlist files. 332 pollForFile(playlistFile); 333 334 // Scan again to verify updated playlist metadata 335 MediaStore.scanFile(context.getContentResolver(), playlistFile); 336 337 try (ContentProviderClient modern = context.getContentResolver() 338 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 339 long legacyPlaylistId = 340 playlistMap.getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID); 341 long legacyAudioId1 = audios[0].getAsLong(MediaColumns._ID); 342 long legacyAudioId2 = audios[1].getAsLong(MediaColumns._ID); 343 344 // Verify that playlist_id matches with legacy playlist_id 345 { 346 Uri playlists = MediaStore.Audio.Playlists.getContentUri(mVolumeName); 347 final String[] project = {FileColumns._ID, MediaStore.Audio.PlaylistsColumns.NAME}; 348 349 try (Cursor cursor = modern.query(playlists, project, null, null, null)) { 350 boolean found = false; 351 while(cursor.moveToNext()) { 352 if (cursor.getLong(0) == legacyPlaylistId) { 353 found = true; 354 assertEquals(playlistName, cursor.getString(1)); 355 break; 356 } 357 } 358 assertTrue(found); 359 } 360 } 361 362 // Verify that playlist_members map matches legacy playlist_members map. 363 { 364 Uri members = MediaStore.Audio.Playlists.Members.getContentUri( 365 mVolumeName, legacyPlaylistId); 366 final String[] project = { MediaStore.Audio.Playlists.Members.AUDIO_ID }; 367 368 try (Cursor cursor = modern.query(members, project, null, null, 369 MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER)) { 370 assertTrue(cursor.moveToNext()); 371 assertEquals(legacyAudioId1, cursor.getLong(0)); 372 assertTrue(cursor.moveToNext()); 373 assertEquals(legacyAudioId2, cursor.getLong(0)); 374 assertFalse(cursor.moveToNext()); 375 } 376 } 377 378 // Verify that migrated playlist audio_id refers to legacy audio file. 379 { 380 Uri modernAudioUri = ContentUris.withAppendedId( 381 MediaStore.Audio.Media.getContentUri(mVolumeName), legacyAudioId1); 382 final String[] project = {FileColumns.DATA}; 383 384 try (Cursor cursor = modern.query(modernAudioUri, project, null, null, null)) { 385 assertTrue(cursor.moveToFirst()); 386 assertEquals(audios[0].getAsString(MediaColumns.DATA), cursor.getString(0)); 387 } 388 } 389 } 390 } 391 prepareProviders(Context context, UiAutomation ui)392 private static void prepareProviders(Context context, UiAutomation ui) throws Exception { 393 final ProviderInfo legacyProvider = context.getPackageManager() 394 .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0); 395 final ProviderInfo modernProvider = context.getPackageManager() 396 .resolveContentProvider(MediaStore.AUTHORITY, 0); 397 398 // Only continue if we have both providers to test against 399 Assume.assumeNotNull(legacyProvider); 400 Assume.assumeNotNull(modernProvider); 401 402 // Clear data on the legacy provider so that we create a database 403 waitForMountedAndIdle(context.getContentResolver()); 404 executeShellCommand("sync", ui); 405 executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui); 406 waitForMountedAndIdle(context.getContentResolver()); 407 } 408 clearProviders(Context context, UiAutomation ui)409 private static void clearProviders(Context context, UiAutomation ui) throws Exception { 410 final ProviderInfo modernProvider = context.getPackageManager() 411 .resolveContentProvider(MediaStore.AUTHORITY, 0); 412 413 // Clear data on the modern provider so that the initial scan recovers 414 // metadata from the legacy provider 415 waitForMountedAndIdle(context.getContentResolver()); 416 executeShellCommand("sync", ui); 417 executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui); 418 waitForMountedAndIdle(context.getContentResolver()); 419 } 420 421 /** 422 * Verify that a legacy database with thousands of media entries can be 423 * successfully migrated. 424 */ 425 @Test testLegacy_Extreme()426 public void testLegacy_Extreme() throws Exception { 427 final Context context = InstrumentationRegistry.getTargetContext(); 428 final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 429 430 prepareProviders(context, ui); 431 432 // Create thousands of items in the legacy provider 433 try (ContentProviderClient legacy = context.getContentResolver() 434 .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) { 435 // We're purposefully "silent" to avoid creating the raw file on 436 // disk, since otherwise this test would take several minutes 437 final Uri insertTarget = rewriteToLegacy( 438 mExternalImages.buildUpon().appendQueryParameter("silent", "true").build()); 439 440 final ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 441 for (int i = 0; i < EXTREME_COUNT; i++) { 442 ops.add(ContentProviderOperation.newInsert(insertTarget) 443 .withValues(generateValues(FileColumns.MEDIA_TYPE_IMAGE, "image/png", 444 Environment.DIRECTORY_PICTURES)) 445 .build()); 446 447 if ((ops.size() > 1_000) || (i == (EXTREME_COUNT - 1))) { 448 Log.v(TAG, "Inserting items..."); 449 legacy.applyBatch(MediaStore.AUTHORITY_LEGACY, ops); 450 ops.clear(); 451 } 452 } 453 } 454 455 clearProviders(context, ui); 456 457 // Confirm that details from legacy provider have migrated 458 try (ContentProviderClient modern = context.getContentResolver() 459 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 460 try (Cursor cursor = modern.query(mExternalImages, null, null, null)) { 461 Truth.assertThat(cursor.getCount()).isAtLeast(EXTREME_COUNT); 462 } 463 } 464 } 465 doLegacy(Uri collectionUri, ContentValues values)466 private void doLegacy(Uri collectionUri, ContentValues values) throws Exception { 467 final Context context = InstrumentationRegistry.getTargetContext(); 468 final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 469 470 prepareProviders(context, ui); 471 472 // Create a well-known entry in legacy provider, and write data into 473 // place to ensure the file is created on disk 474 final Uri legacyUri; 475 final File legacyFile; 476 try (ContentProviderClient legacy = context.getContentResolver() 477 .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) { 478 legacyUri = rewriteToLegacy(legacy.insert(rewriteToLegacy(collectionUri), values)); 479 legacyFile = new File(values.getAsString(MediaColumns.DATA)); 480 481 // Remember our ID to check it later 482 values.put(MediaColumns._ID, legacyUri.getLastPathSegment()); 483 484 // Drop media type from the columns we check, since it's implicitly 485 // verified via the collection Uri 486 values.remove(FileColumns.MEDIA_TYPE); 487 488 // Drop raw path, since we may rename pending or trashed files 489 values.remove(FileColumns.DATA); 490 } 491 492 // This will delete MediaProvider data and restarts MediaProvider, and mounts storage. 493 clearProviders(context, ui); 494 495 // Make sure we do not lose the ORIENTATION column after database migration 496 // We check this column again after the scan 497 if (values.getAsString(MediaColumns.ORIENTATION) != null) { 498 assertOrientationColumn(collectionUri, values, context, legacyFile); 499 } 500 501 // And force a scan to confirm upgraded data survives 502 MediaStore.scanVolume(context.getContentResolver(), 503 MediaStore.getVolumeName(collectionUri)); 504 assertColumnsHaveExpectedValues(collectionUri, values, context, legacyFile); 505 506 } 507 assertOrientationColumn(Uri collectionUri, ContentValues originalValues, Context context, File legacyFile)508 private void assertOrientationColumn(Uri collectionUri, ContentValues originalValues, 509 Context context, File legacyFile) throws Exception { 510 final ContentValues values = new ContentValues(); 511 values.put(MediaColumns.ORIENTATION, (String) originalValues.get(MediaColumns.ORIENTATION)); 512 assertColumnsHaveExpectedValues(collectionUri, values, context, legacyFile); 513 } 514 assertColumnsHaveExpectedValues(Uri collectionUri, ContentValues values, Context context, File legacyFile)515 private void assertColumnsHaveExpectedValues(Uri collectionUri, ContentValues values, 516 Context context, File legacyFile) throws Exception { 517 // Confirm that details from legacy provider have migrated 518 try (ContentProviderClient modern = context.getContentResolver() 519 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 520 final Bundle extras = new Bundle(); 521 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 522 MediaColumns.DISPLAY_NAME + "=?"); 523 extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, 524 new String[] { legacyFile.getName() }); 525 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 526 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 527 extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE); 528 529 try (Cursor cursor = pollForCursor(modern, collectionUri, extras)) { 530 assertNotNull(cursor); 531 assertTrue(cursor.moveToFirst()); 532 for (String key : values.keySet()) { 533 assertWithMessage("Checking key %s", key) 534 .that(cursor.getString(cursor.getColumnIndexOrThrow(key))) 535 .isEqualTo(values.get(key)); 536 } 537 } 538 } 539 } 540 waitForMountedAndIdle(ContentResolver resolver)541 private static void waitForMountedAndIdle(ContentResolver resolver) { 542 // We purposefully perform these operations twice in this specific 543 // order, since clearing the data on a package can asynchronously 544 // perform a vold reset, which can make us think storage is ready and 545 // mounted when it's moments away from being torn down. 546 pollForExternalStorageState(); 547 MediaStore.waitForIdle(resolver); 548 pollForExternalStorageState(); 549 MediaStore.waitForIdle(resolver); 550 } 551 pollForCursor(ContentProviderClient modern, Uri collectionUri, Bundle extras)552 private static Cursor pollForCursor(ContentProviderClient modern, Uri collectionUri, 553 Bundle extras) throws Exception { 554 Cursor cursor = null; 555 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 556 try { 557 cursor = modern.query(collectionUri, null, extras, null); 558 return cursor; 559 } catch (IllegalArgumentException e) { 560 // try again 561 } 562 Log.v(TAG, "Waiting for..." + collectionUri); 563 SystemClock.sleep(POLLING_SLEEP_MILLIS); 564 } 565 fail("Timed out while waiting for uri " + collectionUri); 566 return cursor; 567 } 568 pollForFile(File file)569 private static void pollForFile(File file) { 570 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 571 if (file.exists()) return; 572 573 Log.v(TAG, "Waiting for..." + file); 574 SystemClock.sleep(POLLING_SLEEP_MILLIS); 575 } 576 fail("Timed out while waiting for file " + file); 577 } 578 pollForExternalStorageState()579 private static void pollForExternalStorageState() { 580 final File target = Environment.getExternalStorageDirectory(); 581 for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) { 582 try { 583 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target)) 584 && Os.statvfs(target.getAbsolutePath()).f_blocks > 0) { 585 return; 586 } 587 } catch (ErrnoException ignored) { 588 } 589 590 Log.v(TAG, "Waiting for external storage..."); 591 SystemClock.sleep(POLLING_SLEEP_MILLIS); 592 } 593 fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED"); 594 } 595 executeShellCommand(String command, UiAutomation uiAutomation)596 public static String executeShellCommand(String command, UiAutomation uiAutomation) 597 throws IOException { 598 int attempt = 0; 599 while (attempt++ < 5) { 600 try { 601 return executeShellCommandInternal(command, uiAutomation); 602 } catch (InterruptedIOException e) { 603 // Hmm, we had trouble executing the shell command; the best we 604 // can do is try again a few more times 605 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 606 } 607 } 608 throw new IOException("Failed to execute " + command); 609 } 610 executeShellCommandInternal(String command, UiAutomation uiAutomation)611 public static String executeShellCommandInternal(String command, UiAutomation uiAutomation) 612 throws IOException { 613 Log.v(TAG, "$ " + command); 614 ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString()); 615 BufferedReader br = null; 616 try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) { 617 br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); 618 String str = null; 619 StringBuilder out = new StringBuilder(); 620 while ((str = br.readLine()) != null) { 621 Log.v(TAG, "> " + str); 622 out.append(str); 623 } 624 return out.toString(); 625 } finally { 626 if (br != null) { 627 br.close(); 628 } 629 } 630 } 631 copyFromCursorToContentValues(@onNull String column, @NonNull Cursor cursor, @NonNull ContentValues values)632 public static void copyFromCursorToContentValues(@NonNull String column, @NonNull Cursor cursor, 633 @NonNull ContentValues values) { 634 final int index = cursor.getColumnIndex(column); 635 if (index != -1) { 636 if (cursor.isNull(index)) { 637 values.putNull(column); 638 } else { 639 values.put(column, cursor.getString(index)); 640 } 641 } 642 } 643 } 644