1 /* 2 * Copyright (C) 2018 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; 18 19 import static com.android.providers.media.scan.MediaScannerTest.stage; 20 import static com.android.providers.media.util.FileUtils.extractDisplayName; 21 import static com.android.providers.media.util.FileUtils.extractRelativePath; 22 import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName; 23 import static com.android.providers.media.util.FileUtils.isDownload; 24 import static com.android.providers.media.util.FileUtils.isDownloadDir; 25 26 import static com.google.common.truth.Truth.assertThat; 27 import static com.google.common.truth.Truth.assertWithMessage; 28 29 import static org.junit.Assert.assertArrayEquals; 30 import static org.junit.Assert.assertEquals; 31 import static org.junit.Assert.assertFalse; 32 import static org.junit.Assert.assertNotEquals; 33 import static org.junit.Assert.assertNotNull; 34 import static org.junit.Assert.assertNull; 35 import static org.junit.Assert.assertTrue; 36 import static org.junit.Assert.fail; 37 38 import android.Manifest; 39 import android.content.ContentProviderClient; 40 import android.content.ContentProviderOperation; 41 import android.content.ContentResolver; 42 import android.content.ContentUris; 43 import android.content.ContentValues; 44 import android.content.Context; 45 import android.content.Intent; 46 import android.content.pm.PackageManager; 47 import android.content.pm.ProviderInfo; 48 import android.content.res.AssetFileDescriptor; 49 import android.database.Cursor; 50 import android.database.sqlite.SQLiteException; 51 import android.net.Uri; 52 import android.os.Build; 53 import android.os.Bundle; 54 import android.os.CancellationSignal; 55 import android.os.Environment; 56 import android.os.UserHandle; 57 import android.os.UserManager; 58 import android.provider.MediaStore; 59 import android.provider.MediaStore.Audio.AudioColumns; 60 import android.provider.MediaStore.Files.FileColumns; 61 import android.provider.MediaStore.Images.ImageColumns; 62 import android.provider.MediaStore.MediaColumns; 63 import android.system.ErrnoException; 64 import android.system.Os; 65 import android.system.OsConstants; 66 import android.util.ArrayMap; 67 import android.util.Log; 68 69 import androidx.test.InstrumentationRegistry; 70 import androidx.test.filters.SdkSuppress; 71 import androidx.test.runner.AndroidJUnit4; 72 73 import com.android.providers.media.MediaProvider.FallbackException; 74 import com.android.providers.media.MediaProvider.VolumeArgumentException; 75 import com.android.providers.media.MediaProvider.VolumeNotFoundException; 76 import com.android.providers.media.photopicker.PickerSyncController; 77 import com.android.providers.media.photopicker.data.ItemsProvider; 78 import com.android.providers.media.util.FileUtils; 79 import com.android.providers.media.util.FileUtilsTest; 80 import com.android.providers.media.util.SQLiteQueryBuilder; 81 import com.android.providers.media.util.UserCache; 82 83 import org.junit.AfterClass; 84 import org.junit.Assert; 85 import org.junit.Assume; 86 import org.junit.Before; 87 import org.junit.BeforeClass; 88 import org.junit.Ignore; 89 import org.junit.Test; 90 import org.junit.runner.RunWith; 91 92 import java.io.ByteArrayOutputStream; 93 import java.io.File; 94 import java.io.IOException; 95 import java.io.PrintWriter; 96 import java.nio.charset.StandardCharsets; 97 import java.util.ArrayList; 98 import java.util.Arrays; 99 import java.util.Collection; 100 import java.util.Collections; 101 import java.util.List; 102 import java.util.Locale; 103 import java.util.regex.Pattern; 104 105 @RunWith(AndroidJUnit4.class) 106 public class MediaProviderTest { 107 static final String TAG = "MediaProviderTest"; 108 109 // The test app without permissions 110 static final String PERMISSIONLESS_APP = "com.android.providers.media.testapp.withoutperms"; 111 112 private static Context sIsolatedContext; 113 114 private static ItemsProvider sItemsProvider; 115 private static Context sContext; 116 private static ContentResolver sIsolatedResolver; 117 118 @BeforeClass setUpBeforeClass()119 public static void setUpBeforeClass() { 120 InstrumentationRegistry.getInstrumentation().getUiAutomation() 121 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, 122 Manifest.permission.READ_COMPAT_CHANGE_CONFIG, 123 Manifest.permission.READ_DEVICE_CONFIG, 124 // Adding this to use getUserHandles() api of UserManagerService which 125 // requires either MANAGE_USERS or CREATE_USERS. Since shell does not have 126 // MANAGER_USERS permissions, using CREATE_USERS in test. This works with 127 // MANAGE_USERS permission for MediaProvider module. 128 Manifest.permission.CREATE_USERS, 129 Manifest.permission.INTERACT_ACROSS_USERS); 130 } 131 132 @Before setUp()133 public void setUp() { 134 resetIsolatedContext(); 135 } 136 137 @AfterClass tearDown()138 public static void tearDown() { 139 InstrumentationRegistry.getInstrumentation() 140 .getUiAutomation().dropShellPermissionIdentity(); 141 } 142 143 /** 144 * To fully exercise all our tests, we require that the Cuttlefish emulator 145 * have both emulated primary storage and an SD card be present. 146 */ 147 @Test testCuttlefish()148 public void testCuttlefish() { 149 Assume.assumeTrue(Build.MODEL.contains("Cuttlefish")); 150 151 assertTrue("Cuttlefish must have both emulated storage and an SD card to exercise tests", 152 MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext()) 153 .size() > 1); 154 } 155 156 @Test testSchema()157 public void testSchema() { 158 for (String path : new String[] { 159 "images/media", 160 "images/media/1", 161 "images/thumbnails", 162 "images/thumbnails/1", 163 164 "audio/media", 165 "audio/media/1", 166 "audio/media/1/genres", 167 "audio/media/1/genres/1", 168 "audio/genres", 169 "audio/genres/1", 170 "audio/genres/1/members", 171 "audio/playlists", 172 "audio/playlists/1", 173 "audio/playlists/1/members", 174 "audio/playlists/1/members/1", 175 "audio/artists", 176 "audio/artists/1", 177 "audio/artists/1/albums", 178 "audio/albums", 179 "audio/albums/1", 180 "audio/albumart", 181 "audio/albumart/1", 182 183 "video/media", 184 "video/media/1", 185 "video/thumbnails", 186 "video/thumbnails/1", 187 188 "file", 189 "file/1", 190 191 "downloads", 192 "downloads/1", 193 }) { 194 final Uri probe = MediaStore.AUTHORITY_URI.buildUpon() 195 .appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build(); 196 try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) { 197 assertNotNull("probe", c); 198 } 199 try { 200 sIsolatedResolver.getType(probe); 201 } catch (IllegalStateException tolerated) { 202 } 203 } 204 } 205 206 @Test testLocale()207 public void testLocale() { 208 try (ContentProviderClient cpc = sIsolatedResolver 209 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 210 ((MediaProvider) cpc.getLocalContentProvider()) 211 .onLocaleChanged(); 212 } 213 } 214 215 @Test testDump()216 public void testDump() throws Exception { 217 try (ContentProviderClient cpc = sIsolatedResolver 218 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 219 cpc.getLocalContentProvider().dump(null, 220 new PrintWriter(new ByteArrayOutputStream()), null); 221 } 222 } 223 224 /** 225 * Verify that our fallback exceptions throw on modern apps while degrading 226 * gracefully for legacy apps. 227 */ 228 @Test testFallbackException()229 public void testFallbackException() throws Exception { 230 for (FallbackException e : new FallbackException[] { 231 new FallbackException("test", Build.VERSION_CODES.Q), 232 new VolumeNotFoundException("test"), 233 new VolumeArgumentException(new File("/"), Collections.emptyList()) 234 }) { 235 // Modern apps should get thrown 236 assertThrows(Exception.class, () -> { 237 e.translateForInsert(Build.VERSION_CODES.CUR_DEVELOPMENT); 238 }); 239 assertThrows(Exception.class, () -> { 240 e.translateForUpdateDelete(Build.VERSION_CODES.CUR_DEVELOPMENT); 241 }); 242 assertThrows(Exception.class, () -> { 243 e.translateForQuery(Build.VERSION_CODES.CUR_DEVELOPMENT); 244 }); 245 246 // Legacy apps gracefully log without throwing 247 assertEquals(null, e.translateForInsert(Build.VERSION_CODES.BASE)); 248 assertEquals(0, e.translateForUpdateDelete(Build.VERSION_CODES.BASE)); 249 assertEquals(null, e.translateForQuery(Build.VERSION_CODES.BASE)); 250 } 251 } 252 253 /** 254 * We already have solid coverage of this logic in {@link IdleServiceTest}, 255 * but the coverage system currently doesn't measure that, so we add the 256 * bare minimum local testing here to convince the tooling that it's 257 * covered. 258 */ 259 @Test testIdle()260 public void testIdle() throws Exception { 261 try (ContentProviderClient cpc = sIsolatedResolver 262 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 263 ((MediaProvider) cpc.getLocalContentProvider()) 264 .onIdleMaintenance(new CancellationSignal()); 265 } 266 } 267 268 /** 269 * We already have solid coverage of this logic in 270 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 271 * measure that, so we add the bare minimum local testing here to convince 272 * the tooling that it's covered. 273 */ 274 @Test testCanonicalize()275 public void testCanonicalize() throws Exception { 276 // We might have old files lurking, so force a clean slate 277 resetIsolatedContext(); 278 279 final File dir = Environment 280 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 281 for (File file : new File[] { 282 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")), 283 stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")), 284 stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg")) 285 }) { 286 final Uri uri = MediaStore.scanFile(sIsolatedResolver, file); 287 Log.v(TAG, "Scanned " + file + " as " + uri); 288 289 final Uri forward = sIsolatedResolver.canonicalize(uri); 290 final Uri reverse = sIsolatedResolver.uncanonicalize(forward); 291 292 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(forward)); 293 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(reverse)); 294 } 295 } 296 297 /** 298 * We already have solid coverage of this logic in 299 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 300 * measure that, so we add the bare minimum local testing here to convince 301 * the tooling that it's covered. 302 */ 303 @Test testMetadata()304 public void testMetadata() { 305 assertNotNull(MediaStore.getVersion(sIsolatedContext, 306 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 307 assertNotNull(MediaStore.getGeneration(sIsolatedResolver, 308 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 309 } 310 311 /** 312 * We already have solid coverage of this logic in 313 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 314 * measure that, so we add the bare minimum local testing here to convince 315 * the tooling that it's covered. 316 */ 317 @Test testCreateRequest()318 public void testCreateRequest() throws Exception { 319 final Collection<Uri> uris = Arrays.asList( 320 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)); 321 assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris)); 322 } 323 324 @Test testGrantMediaReadForPackage()325 public void testGrantMediaReadForPackage() throws Exception { 326 final File dir = Environment 327 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 328 final File testFile = stage(R.raw.lg_g4_iso_800_jpg, 329 new File(dir, "test" + System.nanoTime() + ".jpg")); 330 final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile); 331 Long fileId = ContentUris.parseId(uri); 332 333 final Uri.Builder builder = Uri.EMPTY.buildUpon(); 334 builder.scheme("content"); 335 builder.encodedAuthority(MediaStore.AUTHORITY); 336 337 final Uri testUri = builder.appendPath("picker") 338 .appendPath(Integer.toString(UserHandle.myUserId())) 339 .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY) 340 .appendPath(MediaStore.AUTHORITY) 341 .appendPath(Long.toString(fileId)) 342 .build(); 343 344 try { 345 MediaStore.grantMediaReadForPackage(sIsolatedContext, 346 android.os.Process.myUid(), 347 List.of(testUri)); 348 } finally { 349 dir.delete(); 350 testFile.delete(); 351 } 352 353 } 354 355 @Test testGetReadGrantsForPackage()356 public void testGetReadGrantsForPackage() throws Exception { 357 final File dir = Environment 358 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 359 final File testFile = stage(R.raw.lg_g4_iso_800_jpg, 360 new File(dir, "test" + System.nanoTime() + ".jpg")); 361 final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile); 362 Long fileId = ContentUris.parseId(uri); 363 364 final Uri.Builder builder = Uri.EMPTY.buildUpon(); 365 builder.scheme("content"); 366 builder.encodedAuthority(MediaStore.AUTHORITY); 367 368 final Uri testUri = builder.appendPath("picker") 369 .appendPath(Integer.toString(UserHandle.myUserId())) 370 .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY) 371 .appendPath(MediaStore.AUTHORITY) 372 .appendPath(Long.toString(fileId)) 373 .build(); 374 375 try { 376 String[] mimeTypes = {"image/*"}; 377 // Verify empty list with no grants. 378 List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage( 379 android.os.Process.myUid(), mimeTypes); 380 assertTrue(grantedUris.isEmpty()); 381 382 // Grants the READ-GRANT for the testUris for the current package. 383 MediaStore.grantMediaReadForPackage(sIsolatedContext, 384 android.os.Process.myUid(), 385 List.of(testUri)); 386 387 // Assert that the grant was returned. 388 List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage( 389 android.os.Process.myUid(), mimeTypes); 390 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris2.get(0))); 391 } finally { 392 dir.delete(); 393 testFile.delete(); 394 } 395 } 396 397 @Test testRevokeReadGrantsForPackage()398 public void testRevokeReadGrantsForPackage() throws Exception { 399 final File dir = Environment 400 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 401 final File testFile = stage(R.raw.lg_g4_iso_800_jpg, 402 new File(dir, "test" + System.nanoTime() + ".jpg")); 403 final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile); 404 Long fileId = ContentUris.parseId(uri); 405 406 final Uri.Builder builder = Uri.EMPTY.buildUpon(); 407 builder.scheme("content"); 408 builder.encodedAuthority(MediaStore.AUTHORITY); 409 410 final Uri testUri = builder.appendPath("picker") 411 .appendPath(Integer.toString(UserHandle.myUserId())) 412 .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY) 413 .appendPath(MediaStore.AUTHORITY) 414 .appendPath(Long.toString(fileId)) 415 .build(); 416 417 try { 418 String[] mimeTypes = {"image/*"}; 419 MediaStore.grantMediaReadForPackage(sIsolatedContext, 420 android.os.Process.myUid(), 421 List.of(testUri)); 422 List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage( 423 android.os.Process.myUid(), mimeTypes); 424 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris.get(0))); 425 426 // Revoked the grant that was provided to testUri and verify that now the current 427 // package has no grants. 428 MediaStore.revokeMediaReadForPackages(sIsolatedContext, android.os.Process.myUid(), 429 grantedUris); 430 List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage( 431 android.os.Process.myUid(), mimeTypes); 432 assertEquals(0, grantedUris2.size()); 433 } finally { 434 dir.delete(); 435 testFile.delete(); 436 } 437 } 438 439 /** 440 * We already have solid coverage of this logic in 441 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 442 * measure that, so we add the bare minimum local testing here to convince 443 * the tooling that it's covered. 444 */ 445 @Test testCheckUriPermission()446 public void testCheckUriPermission() throws Exception { 447 final ContentValues values = new ContentValues(); 448 values.put(MediaColumns.DISPLAY_NAME, "test.mp3"); 449 values.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 450 final Uri uri = sIsolatedResolver.insert( 451 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values); 452 453 assertEquals(PackageManager.PERMISSION_GRANTED, sIsolatedResolver.checkUriPermission(uri, 454 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)); 455 } 456 457 @Test testTrashLongFileNameItemHasTrimmedFileName()458 public void testTrashLongFileNameItemHasTrimmedFileName() throws Exception { 459 testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_TRASHED); 460 } 461 462 @Test testPendingLongFileNameItemHasTrimmedFileName()463 public void testPendingLongFileNameItemHasTrimmedFileName() throws Exception { 464 testActionLongFileNameItemHasTrimmedFileName(MediaColumns.IS_PENDING); 465 } 466 testActionLongFileNameItemHasTrimmedFileName(String columnKey)467 private void testActionLongFileNameItemHasTrimmedFileName(String columnKey) throws Exception { 468 // We might have old files lurking, so force a clean slate 469 resetIsolatedContext(); 470 final String[] projection = new String[]{MediaColumns.DATA}; 471 final File dir = Environment 472 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 473 474 // create extreme long file name 475 final String originalName = FileUtilsTest.createExtremeFileName("test" + System.nanoTime(), 476 ".jpg"); 477 478 File file = stage(R.raw.lg_g4_iso_800_jpg, new File(dir, originalName)); 479 final Uri uri = MediaStore.scanFile(sIsolatedResolver, file); 480 Log.v(TAG, "Scanned " + file + " as " + uri); 481 482 try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { 483 assertNotNull(c); 484 assertEquals(1, c.getCount()); 485 assertTrue(c.moveToFirst()); 486 final String data = c.getString(0); 487 final String result = FileUtils.extractDisplayName(data); 488 assertEquals(originalName, result); 489 } 490 491 final Bundle extras = new Bundle(); 492 extras.putBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true); 493 final ContentValues values = new ContentValues(); 494 values.put(columnKey, 1); 495 sIsolatedResolver.update(uri, values, extras); 496 497 try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { 498 assertNotNull(c); 499 assertEquals(1, c.getCount()); 500 assertTrue(c.moveToFirst()); 501 final String data = c.getString(0); 502 final String result = FileUtils.extractDisplayName(data); 503 assertThat(result.length()).isAtMost(FileUtilsTest.MAX_FILENAME_BYTES); 504 assertNotEquals(originalName, result); 505 } 506 } 507 508 @Test testInsertionWithFilePathOnAnotherUserVolume_throwsIllegalArgumentException()509 public void testInsertionWithFilePathOnAnotherUserVolume_throwsIllegalArgumentException() { 510 final UserCache userCache = new UserCache(sContext); 511 UserHandle otherUserHandle = sContext.getSystemService(UserManager.class) 512 .getUserHandles(true).stream() 513 .filter(uH -> !userCache.getUsersCached().contains(uH)) 514 .findFirst() 515 .orElse(null); 516 Assume.assumeNotNull(otherUserHandle); 517 518 final ContentValues values = new ContentValues(); 519 values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Download"); 520 final String filePath = "/storage/emulated/" 521 + otherUserHandle.getIdentifier() + "/Pictures/test.jpg"; 522 values.put(MediaStore.Images.Media.DISPLAY_NAME, 523 "./../../../../../../../../../../../" + filePath); 524 525 IllegalArgumentException illegalArgumentException = Assert.assertThrows( 526 IllegalArgumentException.class, () -> sIsolatedResolver.insert( 527 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 528 values)); 529 530 assertThat(illegalArgumentException).hasMessageThat().contains( 531 "Requested path " + filePath + " doesn't appear"); 532 } 533 534 @Test testInsertionWithInvalidFilePath_throwsIllegalArgumentException()535 public void testInsertionWithInvalidFilePath_throwsIllegalArgumentException() { 536 final ContentValues values = new ContentValues(); 537 values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Android/media/com.example/"); 538 values.put(MediaStore.Images.Media.DISPLAY_NAME, "data/media/test.txt"); 539 540 IllegalArgumentException illegalArgumentException = Assert.assertThrows( 541 IllegalArgumentException.class, () -> sIsolatedResolver.insert( 542 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 543 values)); 544 545 assertThat(illegalArgumentException).hasMessageThat().contains( 546 "Primary directory Android not allowed for content://media/external_primary/file;" 547 + " allowed directories are [Download, Documents]"); 548 } 549 550 @Test testUpdationWithInvalidFilePath_throwsIllegalArgumentException()551 public void testUpdationWithInvalidFilePath_throwsIllegalArgumentException() { 552 final ContentValues values = new ContentValues(); 553 values.put(MediaStore.MediaColumns.RELATIVE_PATH, "Download"); 554 values.put(MediaStore.Images.Media.DISPLAY_NAME, "test.txt"); 555 Uri uri = sIsolatedResolver.insert( 556 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 557 values); 558 559 final ContentValues newValues = new ContentValues(); 560 newValues.put(MediaStore.MediaColumns.DATA, "/storage/emulated/0/../../../data/media/"); 561 IllegalArgumentException illegalArgumentException = Assert.assertThrows( 562 IllegalArgumentException.class, 563 () -> sIsolatedResolver.update(uri, newValues, null)); 564 565 assertThat(illegalArgumentException).hasMessageThat().contains( 566 "Requested path /data/media doesn't appear under [/storage/emulated/0]"); 567 } 568 569 /** 570 * We already have solid coverage of this logic in 571 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 572 * measure that, so we add the bare minimum local testing here to convince 573 * the tooling that it's covered. 574 */ 575 @Test testBulkInsert()576 public void testBulkInsert() throws Exception { 577 final ContentValues values1 = new ContentValues(); 578 values1.put(MediaColumns.DISPLAY_NAME, "test1.mp3"); 579 values1.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 580 581 final ContentValues values2 = new ContentValues(); 582 values2.put(MediaColumns.DISPLAY_NAME, "test2.mp3"); 583 values2.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 584 585 final Uri targetUri = MediaStore.Audio.Media 586 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 587 assertEquals(2, sIsolatedResolver.bulkInsert(targetUri, 588 new ContentValues[] { values1, values2 })); 589 } 590 591 /** 592 * We already have solid coverage of this logic in 593 * {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't 594 * measure that, so we add the bare minimum local testing here to convince 595 * the tooling that it's covered. 596 */ 597 @Test testCustomCollator()598 public void testCustomCollator() throws Exception { 599 final Bundle extras = new Bundle(); 600 extras.putString(ContentResolver.QUERY_ARG_SORT_LOCALE, "en"); 601 602 try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI, 603 null, extras, null)) { 604 assertNotNull(c); 605 } 606 } 607 608 /** 609 * This is only for coverage purposes. All logical tests will be included in the 610 * root/cts/hostsidetests/scopedStorage directory. 611 */ 612 @Test testRecentSelectionOnly()613 public void testRecentSelectionOnly() { 614 final Bundle extras = new Bundle(); 615 extras.putBoolean(MediaStore.QUERY_ARG_LATEST_SELECTION_ONLY, true); 616 617 try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI, 618 null, extras, null)) { 619 assertNotNull(c); 620 } 621 } 622 623 @Test testComputeCommonPrefix_Single()624 public void testComputeCommonPrefix_Single() { 625 assertEquals(Uri.parse("content://authority/1/2/3"), 626 MediaProvider.computeCommonPrefix(Arrays.asList( 627 Uri.parse("content://authority/1/2/3")))); 628 } 629 630 @Test testComputeCommonPrefix_Deeper()631 public void testComputeCommonPrefix_Deeper() { 632 assertEquals(Uri.parse("content://authority/1/2/3"), 633 MediaProvider.computeCommonPrefix(Arrays.asList( 634 Uri.parse("content://authority/1/2/3/4"), 635 Uri.parse("content://authority/1/2/3/4/5"), 636 Uri.parse("content://authority/1/2/3")))); 637 } 638 639 @Test testComputeCommonPrefix_Siblings()640 public void testComputeCommonPrefix_Siblings() { 641 assertEquals(Uri.parse("content://authority/1/2"), 642 MediaProvider.computeCommonPrefix(Arrays.asList( 643 Uri.parse("content://authority/1/2/3"), 644 Uri.parse("content://authority/1/2/99")))); 645 } 646 647 @Test testComputeCommonPrefix_Drastic()648 public void testComputeCommonPrefix_Drastic() { 649 assertEquals(Uri.parse("content://authority"), 650 MediaProvider.computeCommonPrefix(Arrays.asList( 651 Uri.parse("content://authority/1/2/3"), 652 Uri.parse("content://authority/99/99/99")))); 653 } 654 getPathOwnerPackageName(String path)655 private static String getPathOwnerPackageName(String path) { 656 return FileUtils.extractPathOwnerPackageName(path); 657 } 658 659 @Test testPathOwnerPackageName_None()660 public void testPathOwnerPackageName_None() throws Exception { 661 assertEquals(null, getPathOwnerPackageName(null)); 662 assertEquals(null, getPathOwnerPackageName("/data/path")); 663 } 664 665 @Test testPathOwnerPackageName_Emulated()666 public void testPathOwnerPackageName_Emulated() throws Exception { 667 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/DCIM/foo.jpg")); 668 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/")); 669 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/data/")); 670 671 assertEquals("com.example", 672 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/")); 673 assertEquals("com.example", 674 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/foo.jpg")); 675 assertEquals("com.example", 676 getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg")); 677 assertEquals("com.example", 678 getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg")); 679 } 680 681 @Test testPathOwnerPackageName_Portable()682 public void testPathOwnerPackageName_Portable() throws Exception { 683 assertEquals(null, getPathOwnerPackageName("/storage/0000-0000/DCIM/foo.jpg")); 684 685 assertEquals("com.example", 686 getPathOwnerPackageName("/storage/0000-0000/Android/data/com.example/foo.jpg")); 687 } 688 689 @Test testBuildData_Simple()690 public void testBuildData_Simple() throws Exception { 691 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 692 assertEndsWith("/Pictures/file.png", 693 buildFile(uri, null, "file", "image/png")); 694 assertEndsWith("/Pictures/file.png", 695 buildFile(uri, null, "file.png", "image/png")); 696 assertEndsWith("/Pictures/file.jpg.png", 697 buildFile(uri, null, "file.jpg", "image/png")); 698 } 699 700 @Test testBuildData_withUserId()701 public void testBuildData_withUserId() throws Exception { 702 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 703 final ContentValues values = new ContentValues(); 704 values.put(MediaColumns.DISPLAY_NAME, "test_userid"); 705 values.put(MediaColumns.MIME_TYPE, "image/png"); 706 Uri result = sIsolatedResolver.insert(uri, values); 707 try (Cursor c = sIsolatedResolver.query(result, 708 new String[]{MediaColumns.DISPLAY_NAME, FileColumns._USER_ID}, 709 null, null)) { 710 assertNotNull(c); 711 assertEquals(1, c.getCount()); 712 assertTrue(c.moveToFirst()); 713 assertEquals("test_userid.png", c.getString(0)); 714 assertEquals(UserHandle.myUserId(), c.getInt(1)); 715 } 716 } 717 718 @Test testSpecialFormatDefaultValue()719 public void testSpecialFormatDefaultValue() throws Exception { 720 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 721 final ContentValues values = new ContentValues(); 722 values.put(MediaColumns.DISPLAY_NAME, "test_specialFormat"); 723 values.put(MediaColumns.MIME_TYPE, "image/png"); 724 Uri result = sIsolatedResolver.insert(uri, values); 725 try (Cursor c = sIsolatedResolver.query(result, 726 new String[]{MediaColumns.DISPLAY_NAME, FileColumns._SPECIAL_FORMAT}, 727 null, null)) { 728 assertNotNull(c); 729 assertEquals(1, c.getCount()); 730 assertTrue(c.moveToFirst()); 731 assertEquals("test_specialFormat.png", c.getString(0)); 732 assertEquals(FileColumns._SPECIAL_FORMAT_NONE, c.getInt(1)); 733 } 734 } 735 736 @Test testBuildData_Primary()737 public void testBuildData_Primary() throws Exception { 738 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 739 assertEndsWith("/DCIM/IMG_1024.JPG", 740 buildFile(uri, Environment.DIRECTORY_DCIM, "IMG_1024.JPG", "image/jpeg")); 741 } 742 743 @Test 744 @Ignore("Enable as part of b/142561358") testBuildData_Secondary()745 public void testBuildData_Secondary() throws Exception { 746 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 747 assertEndsWith("/Pictures/Screenshots/foo.png", 748 buildFile(uri, "Pictures/Screenshots", "foo.png", "image/png")); 749 } 750 751 @Test testBuildData_InvalidNames()752 public void testBuildData_InvalidNames() throws Exception { 753 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 754 assertEndsWith("/Pictures/foo_bar.png", 755 buildFile(uri, null, "foo/bar", "image/png")); 756 assertEndsWith("/Pictures/_.hidden.png", 757 buildFile(uri, null, ".hidden", "image/png")); 758 } 759 760 @Test testBuildData_InvalidTypes()761 public void testBuildData_InvalidTypes() throws Exception { 762 for (String type : new String[] { 763 "audio/foo", "video/foo", "image/foo", "application/foo", "foo/foo" 764 }) { 765 if (!type.startsWith("audio/")) { 766 assertThrows(IllegalArgumentException.class, () -> { 767 buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 768 null, "foo", type); 769 }); 770 } 771 if (!type.startsWith("video/")) { 772 assertThrows(IllegalArgumentException.class, () -> { 773 buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 774 null, "foo", type); 775 }); 776 } 777 if (!type.startsWith("image/")) { 778 assertThrows(IllegalArgumentException.class, () -> { 779 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 780 null, "foo", type); 781 }); 782 } 783 } 784 } 785 786 @Test testBuildData_InvalidSecondaryTypes()787 public void testBuildData_InvalidSecondaryTypes() throws Exception { 788 assertEndsWith("/Pictures/foo.png", 789 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 790 null, "foo.png", "image/*")); 791 792 assertThrows(IllegalArgumentException.class, () -> { 793 buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 794 null, "foo", "video/*"); 795 }); 796 assertThrows(IllegalArgumentException.class, () -> { 797 buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 798 null, "foo.mp4", "audio/*"); 799 }); 800 } 801 802 @Test testBuildData_EmptyTypes()803 public void testBuildData_EmptyTypes() throws Exception { 804 Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 805 assertEndsWith("/Pictures/foo.png", 806 buildFile(uri, null, "foo.png", "")); 807 808 uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 809 assertEndsWith(".mp4", 810 buildFile(uri, null, "", "")); 811 } 812 813 @Test testEnsureFileColumns_InvalidMimeType_targetSdkQ()814 public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception { 815 final MediaProvider provider = new MediaProvider() { 816 @Override 817 public boolean isFuseThread() { 818 return false; 819 } 820 821 @Override 822 public int getCallingPackageTargetSdkVersion() { 823 return Build.VERSION_CODES.Q; 824 } 825 826 @Override 827 protected void storageNativeBootPropertyChangeListener() { 828 // Ignore this as test app cannot read device config 829 } 830 }; 831 832 final ProviderInfo info = sIsolatedContext.getPackageManager() 833 .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA); 834 // Attach providerInfo, to make sure mCallingIdentity can be populated 835 provider.attachInfo(sIsolatedContext, info); 836 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 837 final ContentValues values = new ContentValues(); 838 839 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 840 provider.ensureFileColumns(uri, values); 841 assertMimetype(values, "image/jpeg"); 842 assertDisplayName(values, "pngimage.png.jpg"); 843 844 values.clear(); 845 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 846 values.put(MediaColumns.MIME_TYPE, ""); 847 provider.ensureFileColumns(uri, values); 848 assertMimetype(values, "image/jpeg"); 849 assertDisplayName(values, "pngimage.png.jpg"); 850 851 values.clear(); 852 values.put(MediaColumns.MIME_TYPE, ""); 853 provider.ensureFileColumns(uri, values); 854 assertMimetype(values, "image/jpeg"); 855 856 values.clear(); 857 values.put(MediaColumns.DISPLAY_NAME, "foo.foo"); 858 provider.ensureFileColumns(uri, values); 859 assertMimetype(values, "image/jpeg"); 860 assertDisplayName(values, "foo.foo.jpg"); 861 } 862 863 @Ignore("Enable as part of b/142561358") testBuildData_Charset()864 public void testBuildData_Charset() throws Exception { 865 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 866 assertEndsWith("/Pictures/foo__bar/bar__baz.png", 867 buildFile(uri, "Pictures/foo\0\0bar", "bar::baz.png", "image/png")); 868 } 869 870 @Test testBuildData_Playlists()871 public void testBuildData_Playlists() throws Exception { 872 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 873 assertEndsWith("/Music/my_playlist.m3u", 874 buildFile(uri, null, "my_playlist", "audio/mpegurl")); 875 assertEndsWith("/Movies/my_playlist.pls", 876 buildFile(uri, "Movies", "my_playlist", "audio/x-scpls")); 877 } 878 879 @Test testBuildData_Subtitles()880 public void testBuildData_Subtitles() throws Exception { 881 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 882 assertEndsWith("/Movies/my_subtitle.srt", 883 buildFile(uri, null, "my_subtitle", "application/x-subrip")); 884 assertEndsWith("/Music/my_lyrics.lrc", 885 buildFile(uri, "Music", "my_lyrics", "application/lrc")); 886 } 887 888 @Test testBuildData_Downloads()889 public void testBuildData_Downloads() throws Exception { 890 final Uri uri = MediaStore.Downloads 891 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 892 assertEndsWith("/Download/linux.iso", 893 buildFile(uri, null, "linux.iso", "application/x-iso9660-image")); 894 } 895 896 @Test testBuildData_Pending_FromValues()897 public void testBuildData_Pending_FromValues() throws Exception { 898 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 899 final ContentValues forward = new ContentValues(); 900 forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); 901 forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); 902 forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); 903 forward.put(MediaColumns.IS_PENDING, 1); 904 forward.put(MediaColumns.IS_TRASHED, 0); 905 forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); 906 ensureFileColumns(uri, forward); 907 908 // Requested filename remains intact, but raw path on disk is mutated to 909 // reflect that it's a pending item with a specific expiration time 910 assertEquals("IMG1024.JPG", 911 forward.getAsString(MediaColumns.DISPLAY_NAME)); 912 assertEndsWith(".pending-1577836800-IMG1024.JPG", 913 forward.getAsString(MediaColumns.DATA)); 914 } 915 916 @Test testBuildData_Pending_FromValues_differentLocale()917 public void testBuildData_Pending_FromValues_differentLocale() throws Exception { 918 // See b/174120008 for context. 919 Locale defaultLocale = Locale.getDefault(); 920 try { 921 Locale.setDefault(new Locale("ar", "SA")); 922 testBuildData_Pending_FromValues(); 923 } finally { 924 Locale.setDefault(defaultLocale); 925 } 926 } 927 928 @Test testBuildData_Pending_FromData()929 public void testBuildData_Pending_FromData() throws Exception { 930 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 931 final ContentValues reverse = new ContentValues(); 932 reverse.put(MediaColumns.DATA, 933 "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG"); 934 ensureFileColumns(uri, reverse); 935 936 assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); 937 assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); 938 assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); 939 assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); 940 assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); 941 assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); 942 } 943 944 @Test testBuildData_Trashed_FromValues()945 public void testBuildData_Trashed_FromValues() throws Exception { 946 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 947 final ContentValues forward = new ContentValues(); 948 forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); 949 forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); 950 forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); 951 forward.put(MediaColumns.IS_PENDING, 0); 952 forward.put(MediaColumns.IS_TRASHED, 1); 953 forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); 954 ensureFileColumns(uri, forward); 955 956 // Requested filename remains intact, but raw path on disk is mutated to 957 // reflect that it's a trashed item with a specific expiration time 958 assertEquals("IMG1024.JPG", 959 forward.getAsString(MediaColumns.DISPLAY_NAME)); 960 assertEndsWith(".trashed-1577836800-IMG1024.JPG", 961 forward.getAsString(MediaColumns.DATA)); 962 } 963 964 @Test testBuildData_Trashed_FromValues_differentLocale()965 public void testBuildData_Trashed_FromValues_differentLocale() throws Exception { 966 // See b/174120008 for context. 967 Locale defaultLocale = Locale.getDefault(); 968 try { 969 Locale.setDefault(new Locale("ar", "SA")); 970 testBuildData_Trashed_FromValues(); 971 } finally { 972 Locale.setDefault(defaultLocale); 973 } 974 } 975 976 @Test testBuildData_Trashed_FromData()977 public void testBuildData_Trashed_FromData() throws Exception { 978 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 979 final ContentValues reverse = new ContentValues(); 980 reverse.put(MediaColumns.DATA, 981 "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG"); 982 ensureFileColumns(uri, reverse); 983 984 assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); 985 assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); 986 assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); 987 assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); 988 assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); 989 assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); 990 } 991 992 @Test testGreylist()993 public void testGreylist() throws Exception { 994 assertFalse(isGreylistMatch( 995 "SELECT secret FROM other_table")); 996 997 assertTrue( 998 isGreylistMatch( 999 "case when case when (date_added >= 157680000 and date_added < 1892160000)" 1000 + " then date_added * 1000 when (date_added >= 157680000000 and" 1001 + " date_added < 1892160000000) then date_added when (date_added >=" 1002 + " 157680000000000 and date_added < 1892160000000000) then date_added" 1003 + " / 1000 else 0 end > case when (date_modified >= 157680000 and" 1004 + " date_modified < 1892160000) then date_modified * 1000 when" 1005 + " (date_modified >= 157680000000 and date_modified < 1892160000000)" 1006 + " then date_modified when (date_modified >= 157680000000000 and" 1007 + " date_modified < 1892160000000000) then date_modified / 1000 else 0" 1008 + " end then case when (date_added >= 157680000 and date_added <" 1009 + " 1892160000) then date_added * 1000 when (date_added >= 157680000000" 1010 + " and date_added < 1892160000000) then date_added when (date_added >=" 1011 + " 157680000000000 and date_added < 1892160000000000) then date_added" 1012 + " / 1000 else 0 end else case when (date_modified >= 157680000 and" 1013 + " date_modified < 1892160000) then date_modified * 1000 when" 1014 + " (date_modified >= 157680000000 and date_modified < 1892160000000)" 1015 + " then date_modified when (date_modified >= 157680000000000 and" 1016 + " date_modified < 1892160000000000) then date_modified / 1000 else 0" 1017 + " end end as corrected_added_modified")); 1018 assertTrue( 1019 isGreylistMatch( 1020 "MAX(case when (datetaken >= 157680000 and datetaken < 1892160000) then" 1021 + " datetaken * 1000 when (datetaken >= 157680000000 and datetaken <" 1022 + " 1892160000000) then datetaken when (datetaken >= 157680000000000" 1023 + " and datetaken < 1892160000000000) then datetaken / 1000 else 0" 1024 + " end)")); 1025 assertTrue(isGreylistMatch("0 as orientation")); 1026 assertTrue(isGreylistMatch("\"content://media/internal/audio/media\"")); 1027 } 1028 1029 @Test testGreylist_115845887()1030 public void testGreylist_115845887() { 1031 assertTrue(isGreylistMatch( 1032 "MAX(*)")); 1033 assertTrue(isGreylistMatch( 1034 "MAX(_id)")); 1035 1036 assertTrue(isGreylistMatch( 1037 "sum(column_name)")); 1038 assertFalse(isGreylistMatch( 1039 "SUM(foo+bar)")); 1040 1041 assertTrue(isGreylistMatch( 1042 "count(column_name)")); 1043 assertFalse(isGreylistMatch( 1044 "count(other_table.column_name)")); 1045 } 1046 1047 @Test testGreylist_116489751_116135586_116117120_116084561_116074030_116062802()1048 public void testGreylist_116489751_116135586_116117120_116084561_116074030_116062802() { 1049 assertTrue( 1050 isGreylistMatch( 1051 "MAX(case when (date_added >= 157680000 and date_added < 1892160000) then" 1052 + " date_added * 1000 when (date_added >= 157680000000 and date_added <" 1053 + " 1892160000000) then date_added when (date_added >= 157680000000000" 1054 + " and date_added < 1892160000000000) then date_added / 1000 else 0" 1055 + " end)")); 1056 } 1057 1058 @Test testGreylist_116699470()1059 public void testGreylist_116699470() { 1060 assertTrue( 1061 isGreylistMatch( 1062 "MAX(case when (date_modified >= 157680000 and date_modified < 1892160000)" 1063 + " then date_modified * 1000 when (date_modified >= 157680000000 and" 1064 + " date_modified < 1892160000000) then date_modified when" 1065 + " (date_modified >= 157680000000000 and date_modified <" 1066 + " 1892160000000000) then date_modified / 1000 else 0 end)")); 1067 } 1068 1069 @Test testGreylist_116531759()1070 public void testGreylist_116531759() { 1071 assertTrue(isGreylistMatch( 1072 "count(*)")); 1073 assertTrue(isGreylistMatch( 1074 "COUNT(*)")); 1075 assertFalse(isGreylistMatch( 1076 "xCOUNT(*)")); 1077 assertTrue(isGreylistMatch( 1078 "count(*) AS image_count")); 1079 assertTrue(isGreylistMatch( 1080 "count(_id)")); 1081 assertTrue(isGreylistMatch( 1082 "count(_id) AS image_count")); 1083 1084 assertTrue(isGreylistMatch( 1085 "column_a AS column_b")); 1086 assertFalse(isGreylistMatch( 1087 "other_table.column_a AS column_b")); 1088 } 1089 1090 @Test testGreylist_118475754()1091 public void testGreylist_118475754() { 1092 assertTrue(isGreylistMatch( 1093 "count(*) pcount")); 1094 assertTrue(isGreylistMatch( 1095 "foo AS bar")); 1096 assertTrue(isGreylistMatch( 1097 "foo bar")); 1098 assertTrue(isGreylistMatch( 1099 "count(foo) AS bar")); 1100 assertTrue(isGreylistMatch( 1101 "count(foo) bar")); 1102 } 1103 1104 @Test testGreylist_119522660()1105 public void testGreylist_119522660() { 1106 assertTrue(isGreylistMatch( 1107 "CAST(_id AS TEXT) AS string_id")); 1108 assertTrue(isGreylistMatch( 1109 "cast(_id as text)")); 1110 } 1111 1112 @Test testGreylist_126945991()1113 public void testGreylist_126945991() { 1114 assertTrue(isGreylistMatch( 1115 "substr(_data, length(_data)-length(_display_name), 1) as filename_prevchar")); 1116 } 1117 1118 @Test testGreylist_127900881()1119 public void testGreylist_127900881() { 1120 assertTrue(isGreylistMatch( 1121 "*")); 1122 } 1123 1124 @Test testGreylist_128389972()1125 public void testGreylist_128389972() { 1126 assertTrue(isGreylistMatch( 1127 " count(bucket_id) images_count")); 1128 } 1129 1130 @Test testGreylist_129746861()1131 public void testGreylist_129746861() { 1132 assertTrue( 1133 isGreylistMatch( 1134 "case when (datetaken >= 157680000 and datetaken < 1892160000) then" 1135 + " datetaken * 1000 when (datetaken >= 157680000000 and datetaken <" 1136 + " 1892160000000) then datetaken when (datetaken >= 157680000000000" 1137 + " and datetaken < 1892160000000000) then datetaken / 1000 else 0" 1138 + " end")); 1139 } 1140 1141 @Test testGreylist_114112523()1142 public void testGreylist_114112523() { 1143 assertTrue(isGreylistMatch( 1144 "audio._id AS _id")); 1145 } 1146 1147 @Test testComputeProjection_AggregationAllowed()1148 public void testComputeProjection_AggregationAllowed() throws Exception { 1149 final SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); 1150 final ArrayMap<String, String> map = new ArrayMap<>(); 1151 map.put("external", "internal"); 1152 builder.setProjectionMap(map); 1153 builder.setStrict(true); 1154 builder.setStrictColumns(true); 1155 1156 assertArrayEquals( 1157 new String[] { "internal" }, 1158 builder.computeProjection(null)); 1159 assertArrayEquals( 1160 new String[] { "internal" }, 1161 builder.computeProjection(new String[] { "external" })); 1162 assertThrows(IllegalArgumentException.class, () -> { 1163 builder.computeProjection(new String[] { "internal" }); 1164 }); 1165 assertThrows(IllegalArgumentException.class, () -> { 1166 builder.computeProjection(new String[] { "MIN(internal)" }); 1167 }); 1168 assertArrayEquals( 1169 new String[] { "MIN(internal)" }, 1170 builder.computeProjection(new String[] { "MIN(external)" })); 1171 assertThrows(IllegalArgumentException.class, () -> { 1172 builder.computeProjection(new String[] { "FOO(external)" }); 1173 }); 1174 } 1175 1176 @Test testIsDownload()1177 public void testIsDownload() throws Exception { 1178 assertTrue(isDownload("/storage/emulated/0/Download/colors.png")); 1179 assertTrue(isDownload("/storage/emulated/0/Download/test.pdf")); 1180 assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4")); 1181 assertTrue(isDownload("/storage/0000-0000/Download/foo.txt")); 1182 1183 assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png")); 1184 assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png")); 1185 assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt")); 1186 assertFalse(isDownload("/storage/emulated/0/Download")); 1187 } 1188 1189 @Test testIsDownloadDir()1190 public void testIsDownloadDir() throws Exception { 1191 assertTrue(isDownloadDir("/storage/emulated/0/Download")); 1192 1193 assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png")); 1194 assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/")); 1195 } 1196 1197 @Test testComputeDataValues_Grouped()1198 public void testComputeDataValues_Grouped() throws Exception { 1199 for (String data : new String[] { 1200 "/storage/0000-0000/DCIM/Camera/IMG1024.JPG", 1201 "/storage/0000-0000/DCIM/Camera/iMg1024.JpG", 1202 "/storage/0000-0000/DCIM/Camera/IMG1024.CR2", 1203 "/storage/0000-0000/DCIM/Camera/IMG1024.BURST001.JPG", 1204 }) { 1205 final ContentValues values = computeDataValues(data); 1206 assertVolume(values, "0000-0000"); 1207 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 1208 assertRelativePath(values, "DCIM/Camera/"); 1209 } 1210 } 1211 1212 @Test testComputeDataValues_Extensions()1213 public void testComputeDataValues_Extensions() throws Exception { 1214 ContentValues values; 1215 1216 values = computeDataValues("/storage/0000-0000/DCIM/Camera/IMG1024"); 1217 assertVolume(values, "0000-0000"); 1218 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 1219 assertRelativePath(values, "DCIM/Camera/"); 1220 1221 values = computeDataValues("/storage/0000-0000/DCIM/Camera/.foo"); 1222 assertVolume(values, "0000-0000"); 1223 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 1224 assertRelativePath(values, "DCIM/Camera/"); 1225 1226 values = computeDataValues("/storage/476A-17F8/123456/test.png"); 1227 assertVolume(values, "476a-17f8"); 1228 assertBucket(values, "/storage/476A-17F8/123456", "123456"); 1229 assertRelativePath(values, "123456/"); 1230 1231 values = computeDataValues("/storage/476A-17F8/123456/789/test.mp3"); 1232 assertVolume(values, "476a-17f8"); 1233 assertBucket(values, "/storage/476A-17F8/123456/789", "789"); 1234 assertRelativePath(values, "123456/789/"); 1235 } 1236 1237 @Test testComputeDataValues_DirectoriesInvalid()1238 public void testComputeDataValues_DirectoriesInvalid() throws Exception { 1239 for (String data : new String[] { 1240 "/storage/IMG1024.JPG", 1241 "/data/media/IMG1024.JPG", 1242 "IMG1024.JPG", 1243 }) { 1244 final ContentValues values = computeDataValues(data); 1245 assertRelativePath(values, null); 1246 } 1247 } 1248 1249 @Test testComputeDataValues_Directories()1250 public void testComputeDataValues_Directories() throws Exception { 1251 ContentValues values; 1252 1253 for (String top : new String[] { 1254 "/storage/emulated/0", 1255 }) { 1256 values = computeDataValues(top + "/IMG1024.JPG"); 1257 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1258 assertBucket(values, top, null); 1259 assertRelativePath(values, "/"); 1260 1261 values = computeDataValues(top + "/One/IMG1024.JPG"); 1262 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1263 assertBucket(values, top + "/One", "One"); 1264 assertRelativePath(values, "One/"); 1265 1266 values = computeDataValues(top + "/One/Two/IMG1024.JPG"); 1267 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1268 assertBucket(values, top + "/One/Two", "Two"); 1269 assertRelativePath(values, "One/Two/"); 1270 1271 values = computeDataValues(top + "/One/Two/Three/IMG1024.JPG"); 1272 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1273 assertBucket(values, top + "/One/Two/Three", "Three"); 1274 assertRelativePath(values, "One/Two/Three/"); 1275 } 1276 } 1277 1278 @Test testEnsureFileColumns_resolvesMimeType()1279 public void testEnsureFileColumns_resolvesMimeType() throws Exception { 1280 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1281 final ContentValues values = new ContentValues(); 1282 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 1283 1284 final MediaProvider provider = new MediaProvider() { 1285 @Override 1286 public boolean isFuseThread() { 1287 return false; 1288 } 1289 1290 @Override 1291 public int getCallingPackageTargetSdkVersion() { 1292 return Build.VERSION_CODES.CUR_DEVELOPMENT; 1293 } 1294 1295 @Override 1296 protected void storageNativeBootPropertyChangeListener() { 1297 // Ignore this as test app cannot read device config 1298 } 1299 }; 1300 final ProviderInfo info = sIsolatedContext.getPackageManager() 1301 .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA); 1302 // Attach providerInfo, to make sure mCallingIdentity can be populated 1303 provider.attachInfo(sIsolatedContext, info); 1304 provider.ensureFileColumns(uri, values); 1305 1306 assertMimetype(values, "image/png"); 1307 } 1308 1309 @Test testRelativePathForInvalidDirectories()1310 public void testRelativePathForInvalidDirectories() throws Exception { 1311 for (String path : new String[] { 1312 "/storage/emulated", 1313 "/storage", 1314 "/data/media/Foo.jpg", 1315 "Foo.jpg", 1316 "storage/Foo" 1317 }) { 1318 assertEquals(null, FileUtils.extractRelativePathWithDisplayName(path)); 1319 } 1320 } 1321 1322 @Test testRelativePathForValidDirectories()1323 public void testRelativePathForValidDirectories() throws Exception { 1324 for (String prefix : new String[] { 1325 "/storage/emulated/0", 1326 "/storage/emulated/10", 1327 "/storage/ABCD-1234" 1328 }) { 1329 assertRelativePathForDirectory(prefix, "/"); 1330 assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/"); 1331 assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/"); 1332 assertRelativePathForDirectory(prefix + "/Z", "Z/"); 1333 assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo", 1334 "Android/media/com.example/Foo/"); 1335 } 1336 } 1337 1338 @Test testComputeAudioKeyValues_167339595_differentAlbumIds()1339 public void testComputeAudioKeyValues_167339595_differentAlbumIds() throws Exception { 1340 // same album name, different album artists 1341 final ContentValues valuesOne = new ContentValues(); 1342 valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1343 valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1344 valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3"); 1345 valuesOne.put(AudioColumns.TITLE, "Clocks"); 1346 valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood"); 1347 valuesOne.put(AudioColumns.ALBUM_ARTIST, "Coldplay"); 1348 valuesOne.put(AudioColumns.GENRE, "Rock"); 1349 valuesOne.put(AudioColumns.IS_MUSIC, true); 1350 1351 final ContentValues valuesTwo = new ContentValues(); 1352 valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1353 valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1354 valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3"); 1355 valuesTwo.put(AudioColumns.TITLE, "Sounds"); 1356 valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood"); 1357 valuesTwo.put(AudioColumns.ALBUM_ARTIST, "ColdplayTwo"); 1358 valuesTwo.put(AudioColumns.GENRE, "Alternative rock"); 1359 valuesTwo.put(AudioColumns.IS_MUSIC, true); 1360 1361 MediaProvider.computeAudioKeyValues(valuesOne); 1362 final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID); 1363 MediaProvider.computeAudioKeyValues(valuesTwo); 1364 final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID); 1365 1366 assertNotEquals(albumIdOne, albumIdTwo); 1367 1368 // same album name, different paths, no album artists 1369 final ContentValues valuesThree = new ContentValues(); 1370 valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1371 valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1372 valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3"); 1373 valuesThree.put(AudioColumns.TITLE, "Silent"); 1374 valuesThree.put(AudioColumns.ALBUM, "Rainbow"); 1375 valuesThree.put(AudioColumns.ARTIST, "Sample1"); 1376 valuesThree.put(AudioColumns.GENRE, "Rock"); 1377 valuesThree.put(AudioColumns.IS_MUSIC, true); 1378 1379 final ContentValues valuesFour = new ContentValues(); 1380 valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1381 valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1382 valuesFour.put(FileColumns.DATA, "/storage/emulated/0/123456/Rainbow.mp3"); 1383 valuesFour.put(AudioColumns.TITLE, "Rainbow"); 1384 valuesFour.put(AudioColumns.ALBUM, "Rainbow"); 1385 valuesFour.put(AudioColumns.ARTIST, "Sample2"); 1386 valuesFour.put(AudioColumns.GENRE, "Alternative rock"); 1387 valuesFour.put(AudioColumns.IS_MUSIC, true); 1388 1389 MediaProvider.computeAudioKeyValues(valuesThree); 1390 final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID); 1391 MediaProvider.computeAudioKeyValues(valuesFour); 1392 final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID); 1393 1394 assertNotEquals(albumIdThree, albumIdFour); 1395 } 1396 1397 @Test testComputeAudioKeyValues_167339595_sameAlbumId()1398 public void testComputeAudioKeyValues_167339595_sameAlbumId() throws Exception { 1399 // same album name, same path, no album artists 1400 final ContentValues valuesOne = new ContentValues(); 1401 valuesOne.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1402 valuesOne.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1403 valuesOne.put(FileColumns.DATA, "/storage/emulated/0/Clocks.mp3"); 1404 valuesOne.put(AudioColumns.TITLE, "Clocks"); 1405 valuesOne.put(AudioColumns.ALBUM, "A Rush of Blood"); 1406 valuesOne.put(AudioColumns.GENRE, "Rock"); 1407 valuesOne.put(AudioColumns.IS_MUSIC, true); 1408 1409 final ContentValues valuesTwo = new ContentValues(); 1410 valuesTwo.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1411 valuesTwo.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1412 valuesTwo.put(FileColumns.DATA, "/storage/emulated/0/Sounds.mp3"); 1413 valuesTwo.put(AudioColumns.TITLE, "Sounds"); 1414 valuesTwo.put(AudioColumns.ALBUM, "A Rush of Blood"); 1415 valuesTwo.put(AudioColumns.GENRE, "Alternative rock"); 1416 valuesTwo.put(AudioColumns.IS_MUSIC, true); 1417 1418 MediaProvider.computeAudioKeyValues(valuesOne); 1419 final long albumIdOne = valuesOne.getAsLong(AudioColumns.ALBUM_ID); 1420 MediaProvider.computeAudioKeyValues(valuesTwo); 1421 final long albumIdTwo = valuesTwo.getAsLong(AudioColumns.ALBUM_ID); 1422 1423 assertEquals(albumIdOne, albumIdTwo); 1424 1425 // same album name, same album artists, different artists 1426 final ContentValues valuesThree = new ContentValues(); 1427 valuesThree.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1428 valuesThree.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1429 valuesThree.put(FileColumns.DATA, "/storage/emulated/0/Silent.mp3"); 1430 valuesThree.put(AudioColumns.TITLE, "Silent"); 1431 valuesThree.put(AudioColumns.ALBUM, "Rainbow"); 1432 valuesThree.put(AudioColumns.ALBUM_ARTIST, "Various Artists"); 1433 valuesThree.put(AudioColumns.ARTIST, "Sample1"); 1434 valuesThree.put(AudioColumns.GENRE, "Rock"); 1435 valuesThree.put(AudioColumns.IS_MUSIC, true); 1436 1437 final ContentValues valuesFour = new ContentValues(); 1438 valuesFour.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO); 1439 valuesFour.put(FileColumns.VOLUME_NAME, MediaStore.VOLUME_EXTERNAL_PRIMARY); 1440 valuesFour.put(FileColumns.DATA, "/storage/emulated/0/Rainbow.mp3"); 1441 valuesFour.put(AudioColumns.TITLE, "Rainbow"); 1442 valuesFour.put(AudioColumns.ALBUM, "Rainbow"); 1443 valuesFour.put(AudioColumns.ALBUM_ARTIST, "Various Artists"); 1444 valuesFour.put(AudioColumns.ARTIST, "Sample2"); 1445 valuesFour.put(AudioColumns.GENRE, "Alternative rock"); 1446 valuesFour.put(AudioColumns.IS_MUSIC, true); 1447 1448 MediaProvider.computeAudioKeyValues(valuesThree); 1449 final long albumIdThree = valuesThree.getAsLong(AudioColumns.ALBUM_ID); 1450 MediaProvider.computeAudioKeyValues(valuesFour); 1451 final long albumIdFour = valuesFour.getAsLong(AudioColumns.ALBUM_ID); 1452 1453 assertEquals(albumIdThree, albumIdFour); 1454 } 1455 1456 @Test testQueryAudioViewsNoTrashedItem()1457 public void testQueryAudioViewsNoTrashedItem() throws Exception { 1458 testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_TRASHED); 1459 } 1460 1461 @Test testQueryAudioViewsNoPendingItem()1462 public void testQueryAudioViewsNoPendingItem() throws Exception { 1463 testQueryAudioViewsNoItemWithColumn(MediaStore.Audio.Media.IS_PENDING); 1464 } 1465 testQueryAudioViewsNoItemWithColumn(String columnKey)1466 private void testQueryAudioViewsNoItemWithColumn(String columnKey) throws Exception { 1467 // We might have old files lurking, so force a clean slate 1468 resetIsolatedContext(); 1469 1470 final File dir = Environment 1471 .getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); 1472 1473 final File audio = new File(dir, "test" + System.nanoTime() + ".mp3"); 1474 final Uri audioUri = 1475 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1476 final String album = "TestAlbum" + System.nanoTime(); 1477 final String artist = "TestArtist" + System.nanoTime(); 1478 final String genre = "TestGenre" + System.nanoTime(); 1479 final String relativePath = extractRelativePath(audio.getAbsolutePath()); 1480 final String displayName = extractDisplayName(audio.getAbsolutePath()); 1481 ContentValues values = new ContentValues(); 1482 1483 values.put(MediaStore.Audio.Media.ALBUM, album); 1484 values.put(MediaStore.Audio.Media.ARTIST, artist); 1485 values.put(MediaStore.Audio.Media.GENRE, genre); 1486 values.put(MediaStore.Audio.Media.DISPLAY_NAME, displayName); 1487 values.put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath); 1488 values.put(MediaStore.Audio.Media.IS_MUSIC, 1); 1489 values.put(columnKey, 1); 1490 1491 Uri result = sIsolatedResolver.insert(audioUri, values); 1492 1493 final long genreId; 1494 // Check the audio file is inserted correctly 1495 try (Cursor c = sIsolatedResolver.query(result, 1496 new String[]{MediaColumns.DISPLAY_NAME, AudioColumns.GENRE_ID, columnKey}, 1497 null, null)) { 1498 assertNotNull(c); 1499 assertEquals(1, c.getCount()); 1500 assertTrue(c.moveToFirst()); 1501 assertEquals(displayName, c.getString(0)); 1502 assertEquals(1, c.getInt(2)); 1503 genreId = c.getLong(1); 1504 } 1505 1506 final String volume = MediaStore.VOLUME_EXTERNAL_PRIMARY; 1507 assertQueryResultNoItems(MediaStore.Audio.Albums.getContentUri(volume)); 1508 assertQueryResultNoItems(MediaStore.Audio.Artists.getContentUri(volume)); 1509 assertQueryResultNoItems(MediaStore.Audio.Genres.getContentUri(volume)); 1510 assertQueryResultNoItems(MediaStore.Audio.Genres.Members.getContentUri(volume, genreId)); 1511 } 1512 1513 @Test 1514 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, maxSdkVersion = Build.VERSION_CODES.R) 1515 @Ignore("b/211068960") testQueryAudioTableNoIsRecordingColumnInR()1516 public void testQueryAudioTableNoIsRecordingColumnInR() throws Exception { 1517 final File file = createAudioRecordingFile(); 1518 final Uri audioUri = 1519 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1520 1521 try (Cursor c = sIsolatedResolver.query(audioUri, null, null, null, null)) { 1522 assertThat(c).isNotNull(); 1523 assertThat(c.getCount()).isEqualTo(1); 1524 assertThat(c.getColumnIndex("is_recording")).isEqualTo(-1); 1525 } finally { 1526 file.delete(); 1527 final File dir = file.getParentFile(); 1528 dir.delete(); 1529 } 1530 } 1531 1532 @Test 1533 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, maxSdkVersion = Build.VERSION_CODES.R) 1534 @Ignore("b/211068960") testQueryIsRecordingInAudioTableExceptionInR()1535 public void testQueryIsRecordingInAudioTableExceptionInR() throws Exception { 1536 final File file = createAudioRecordingFile(); 1537 final Uri audioUri = 1538 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1539 final String[] projection = new String[]{"is_recording"}; 1540 1541 try (Cursor c = sIsolatedResolver.query(audioUri, projection, null, null, null)) { 1542 fail("Expected exception with the is_recording is not a column in Audio table"); 1543 } catch (IllegalArgumentException | SQLiteException expected) { 1544 } finally { 1545 file.delete(); 1546 final File dir = file.getParentFile(); 1547 dir.delete(); 1548 } 1549 } 1550 1551 @Test 1552 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) testQueryAudioTableHasIsRecordingColumnAfterR()1553 public void testQueryAudioTableHasIsRecordingColumnAfterR() throws Exception { 1554 final File file = createAudioRecordingFile(); 1555 final Uri audioUri = 1556 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1557 1558 try (Cursor c = sIsolatedResolver.query(audioUri, null, null, null, null)) { 1559 assertThat(c).isNotNull(); 1560 assertThat(c.getCount()).isEqualTo(1); 1561 final int columnIndex = c.getColumnIndex(AudioColumns.IS_RECORDING); 1562 assertThat(columnIndex).isNotEqualTo(-1); 1563 assertThat(c.moveToFirst()).isTrue(); 1564 assertThat(c.getInt(columnIndex)).isEqualTo(1); 1565 } finally { 1566 file.delete(); 1567 final File dir = file.getParentFile(); 1568 dir.delete(); 1569 } 1570 } 1571 1572 @Test 1573 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) testQueryIsRecordingInAudioTableAfterR()1574 public void testQueryIsRecordingInAudioTableAfterR() throws Exception { 1575 final File file = createAudioRecordingFile(); 1576 final Uri audioUri = 1577 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 1578 final String[] projection = new String[]{AudioColumns.IS_RECORDING}; 1579 1580 try (Cursor c = sIsolatedResolver.query(audioUri, projection, null, null, null)) { 1581 assertThat(c).isNotNull(); 1582 assertThat(c.getCount()).isEqualTo(1); 1583 assertThat(c.moveToFirst()).isTrue(); 1584 assertThat(c.getInt(0)).isEqualTo(1); 1585 } finally { 1586 file.delete(); 1587 final File dir = file.getParentFile(); 1588 dir.delete(); 1589 } 1590 } 1591 createAudioRecordingFile()1592 private File createAudioRecordingFile() throws Exception { 1593 // We might have old files lurking, so force a clean slate 1594 resetIsolatedContext(); 1595 final File dir = Environment 1596 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 1597 final File recordingDir = new File(dir, "Recordings"); 1598 recordingDir.mkdirs(); 1599 final String displayName = "test" + System.nanoTime() + ".mp3"; 1600 final File audio = new File(recordingDir, displayName); 1601 stage(R.raw.test_audio, audio); 1602 final Uri result = MediaStore.scanFile(sIsolatedResolver, audio); 1603 1604 // Check the audio music file exists 1605 try (Cursor c = sIsolatedResolver.query(result, 1606 new String[]{MediaColumns.DISPLAY_NAME, AudioColumns.IS_MUSIC}, null, null)) { 1607 assertThat(c).isNotNull(); 1608 assertThat(c.getCount()).isEqualTo(1); 1609 assertThat(c.moveToFirst()).isTrue(); 1610 assertThat(c.getString(0)).isEqualTo(displayName); 1611 assertThat(c.getInt(1)).isEqualTo(0); 1612 } 1613 return audio; 1614 } 1615 assertQueryResultNoItems(Uri uri)1616 private static void assertQueryResultNoItems(Uri uri) throws Exception { 1617 try (Cursor c = sIsolatedResolver.query(uri, null, null, null, null)) { 1618 assertNotNull(c); 1619 assertEquals(0, c.getCount()); 1620 } 1621 } 1622 assertRelativePathForDirectory(String directoryPath, String relativePath)1623 private static void assertRelativePathForDirectory(String directoryPath, String relativePath) { 1624 assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :") 1625 .that(extractRelativePathWithDisplayName(directoryPath)) 1626 .isEqualTo(relativePath); 1627 } 1628 computeDataValues(String path)1629 private static ContentValues computeDataValues(String path) { 1630 final ContentValues values = new ContentValues(); 1631 values.put(MediaColumns.DATA, path); 1632 FileUtils.computeValuesFromData(values, /*forFuse*/ false); 1633 Log.v(TAG, "Computed values " + values); 1634 return values; 1635 } 1636 assertBucket(ContentValues values, String bucketId, String bucketName)1637 private static void assertBucket(ContentValues values, String bucketId, String bucketName) { 1638 if (bucketId != null) { 1639 assertEquals(bucketName, 1640 values.getAsString(ImageColumns.BUCKET_DISPLAY_NAME)); 1641 assertEquals(bucketId.toLowerCase(Locale.ROOT).hashCode(), 1642 (long) values.getAsLong(ImageColumns.BUCKET_ID)); 1643 } else { 1644 assertNull(values.get(ImageColumns.BUCKET_DISPLAY_NAME)); 1645 assertNull(values.get(ImageColumns.BUCKET_ID)); 1646 } 1647 } 1648 assertVolume(ContentValues values, String volumeName)1649 private static void assertVolume(ContentValues values, String volumeName) { 1650 assertEquals(volumeName, values.getAsString(ImageColumns.VOLUME_NAME)); 1651 } 1652 assertRelativePath(ContentValues values, String relativePath)1653 private static void assertRelativePath(ContentValues values, String relativePath) { 1654 assertEquals(relativePath, values.get(ImageColumns.RELATIVE_PATH)); 1655 } 1656 assertMimetype(ContentValues values, String type)1657 private static void assertMimetype(ContentValues values, String type) { 1658 assertEquals(type, values.get(MediaColumns.MIME_TYPE)); 1659 } 1660 assertDisplayName(ContentValues values, String type)1661 private static void assertDisplayName(ContentValues values, String type) { 1662 assertEquals(type, values.get(MediaColumns.DISPLAY_NAME)); 1663 } 1664 isGreylistMatch(String raw)1665 private static boolean isGreylistMatch(String raw) { 1666 for (Pattern p : MediaProvider.sAllowlist) { 1667 if (p.matcher(raw).matches()) { 1668 return true; 1669 } 1670 } 1671 return false; 1672 } 1673 buildFile(Uri uri, String relativePath, String displayName, String mimeType)1674 private String buildFile(Uri uri, String relativePath, String displayName, 1675 String mimeType) { 1676 final ContentValues values = new ContentValues(); 1677 if (relativePath != null) { 1678 values.put(MediaColumns.RELATIVE_PATH, relativePath); 1679 } 1680 values.put(MediaColumns.DISPLAY_NAME, displayName); 1681 values.put(MediaColumns.MIME_TYPE, mimeType); 1682 try { 1683 ensureFileColumns(uri, values); 1684 } catch (VolumeArgumentException | VolumeNotFoundException e) { 1685 throw e.rethrowAsIllegalArgumentException(); 1686 } 1687 return values.getAsString(MediaColumns.DATA); 1688 } 1689 ensureFileColumns(Uri uri, ContentValues values)1690 private void ensureFileColumns(Uri uri, ContentValues values) 1691 throws VolumeArgumentException, VolumeNotFoundException { 1692 try (ContentProviderClient cpc = sIsolatedResolver 1693 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1694 ((MediaProvider) cpc.getLocalContentProvider()) 1695 .ensureFileColumns(uri, values); 1696 } 1697 } 1698 assertEndsWith(String expected, String actual)1699 private static void assertEndsWith(String expected, String actual) { 1700 if (!actual.endsWith(expected)) { 1701 fail("Expected ends with " + expected + " but found " + actual); 1702 } 1703 } 1704 assertThrows(Class<T> clazz, Runnable r)1705 private static <T extends Exception> void assertThrows(Class<T> clazz, Runnable r) { 1706 try { 1707 r.run(); 1708 fail("Expected " + clazz + " to be thrown"); 1709 } catch (Exception e) { 1710 if (!clazz.isAssignableFrom(e.getClass())) { 1711 throw e; 1712 } 1713 } 1714 } 1715 1716 @Test testNestedTransaction_applyBatch()1717 public void testNestedTransaction_applyBatch() throws Exception { 1718 final Uri[] uris = new Uri[]{ 1719 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, 0), 1720 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 0), 1721 }; 1722 final ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 1723 ops.add(ContentProviderOperation.newDelete(uris[0]).build()); 1724 ops.add(ContentProviderOperation.newDelete(uris[1]).build()); 1725 sIsolatedResolver.applyBatch(MediaStore.AUTHORITY, ops); 1726 } 1727 1728 @Test testRedactionForInvalidUris()1729 public void testRedactionForInvalidUris() throws Exception { 1730 try (ContentProviderClient cpc = sIsolatedResolver 1731 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1732 MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); 1733 final String volumeName = MediaStore.VOLUME_EXTERNAL; 1734 assertNull(mp.getRedactedUri(MediaStore.Images.Media.getContentUri(volumeName))); 1735 assertNull(mp.getRedactedUri(MediaStore.Video.Media.getContentUri(volumeName))); 1736 assertNull(mp.getRedactedUri(MediaStore.Audio.Media.getContentUri(volumeName))); 1737 assertNull(mp.getRedactedUri(MediaStore.Audio.Albums.getContentUri(volumeName))); 1738 assertNull(mp.getRedactedUri(MediaStore.Audio.Artists.getContentUri(volumeName))); 1739 assertNull(mp.getRedactedUri(MediaStore.Audio.Genres.getContentUri(volumeName))); 1740 assertNull(mp.getRedactedUri(MediaStore.Audio.Playlists.getContentUri(volumeName))); 1741 assertNull(mp.getRedactedUri(MediaStore.Downloads.getContentUri(volumeName))); 1742 assertNull(mp.getRedactedUri(MediaStore.Files.getContentUri(volumeName))); 1743 1744 // Check with a very large value - which shouldn't be present normally (at least for 1745 // tests). 1746 assertNull(mp.getRedactedUri( 1747 MediaStore.Images.Media.getContentUri(volumeName, Long.MAX_VALUE))); 1748 } 1749 } 1750 1751 @Test testRedactionForInvalidAndValidUris()1752 public void testRedactionForInvalidAndValidUris() throws Exception { 1753 final String volumeName = MediaStore.VOLUME_EXTERNAL; 1754 final List<Uri> uris = new ArrayList<>(); 1755 uris.add(MediaStore.Images.Media.getContentUri(volumeName)); 1756 uris.add(MediaStore.Video.Media.getContentUri(volumeName)); 1757 1758 final File dir = Environment 1759 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 1760 final File[] files = new File[]{ 1761 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")), 1762 stage(R.raw.test_video_xmp, 1763 new File(dir, "test" + System.nanoTime() + ".mp4")), 1764 stage(R.raw.lg_g4_iso_800_jpg, 1765 new File(dir, "test" + System.nanoTime() + ".jpg")) 1766 }; 1767 1768 try (ContentProviderClient cpc = sIsolatedResolver 1769 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1770 MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); 1771 for (File file : files) { 1772 uris.add(MediaStore.scanFile(sIsolatedResolver, file)); 1773 } 1774 1775 List<Uri> redactedUris = mp.getRedactedUri(uris); 1776 assertEquals(uris.size(), redactedUris.size()); 1777 assertNull(redactedUris.get(0)); 1778 assertNull(redactedUris.get(1)); 1779 assertNotNull(redactedUris.get(2)); 1780 assertNotNull(redactedUris.get(3)); 1781 assertNotNull(redactedUris.get(4)); 1782 } finally { 1783 for (File file : files) { 1784 file.delete(); 1785 } 1786 } 1787 } 1788 1789 @Test testRedactionForFileExtension()1790 public void testRedactionForFileExtension() throws Exception { 1791 testRedactionForFileExtension(R.raw.test_audio, ".mp3"); 1792 testRedactionForFileExtension(R.raw.test_video_xmp, ".mp4"); 1793 testRedactionForFileExtension(R.raw.lg_g4_iso_800_jpg, ".jpg"); 1794 } 1795 1796 @Test testOpenTypedAssetFile_setModeInBundle_failsWrite()1797 public void testOpenTypedAssetFile_setModeInBundle_failsWrite() throws IOException { 1798 final File dir = Environment 1799 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); 1800 final File file = new File(dir, "test" + System.nanoTime() + ".txt"); 1801 stage(R.raw.test_txt, file); 1802 Uri mediaUri = MediaStore.scanFile(sContext.getContentResolver(), file); 1803 Bundle opts = new Bundle(); 1804 opts.putString(MediaStore.EXTRA_MODE, "w"); 1805 1806 try (AssetFileDescriptor afd = sContext.getContentResolver().openTypedAssetFile(mediaUri, 1807 "*/*", opts, null)) { 1808 String rawText = "Hello"; 1809 Os.write(afd.getFileDescriptor(), rawText.getBytes(StandardCharsets.UTF_8), 1810 0, rawText.length()); 1811 fail("Expected failure in write to fail with ErrnoException."); 1812 } catch (ErrnoException expected) { 1813 // Expecting ErrnoException: Bad File Descriptor. Mode set in bundle would not be 1814 // respected if calling app is not MediaProvider itself. 1815 assertThat(expected.errno).isEqualTo(OsConstants.EBADF); 1816 } finally { 1817 file.delete(); 1818 } 1819 } 1820 testRedactionForFileExtension(int resId, String extension)1821 private void testRedactionForFileExtension(int resId, String extension) throws Exception { 1822 final File dir = Environment 1823 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 1824 final File file = new File(dir, "test" + System.nanoTime() + extension); 1825 1826 stage(resId, file); 1827 1828 final List<Uri> uris = new ArrayList<>(); 1829 uris.add(MediaStore.scanFile(sIsolatedResolver, file)); 1830 1831 1832 try (ContentProviderClient cpc = sIsolatedResolver 1833 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1834 final MediaProvider mp = (MediaProvider) cpc.getLocalContentProvider(); 1835 1836 final String[] projection = new String[]{MediaColumns.DISPLAY_NAME, MediaColumns.DATA}; 1837 for (Uri uri : mp.getRedactedUri(uris)) { 1838 try (Cursor c = sIsolatedResolver.query(uri, projection, null, null)) { 1839 assertNotNull(c); 1840 assertEquals(1, c.getCount()); 1841 assertTrue(c.moveToFirst()); 1842 assertTrue(c.getString(0).endsWith(extension)); 1843 assertTrue(c.getString(1).endsWith(extension)); 1844 } 1845 } 1846 } finally { 1847 file.delete(); 1848 } 1849 } 1850 resetIsolatedContext()1851 private static void resetIsolatedContext() { 1852 if (sIsolatedResolver != null) { 1853 // This is necessary, we wait for all unfinished tasks to finish before we create a 1854 // new IsolatedContext. 1855 MediaStore.waitForIdle(sIsolatedResolver); 1856 } 1857 1858 sContext = InstrumentationRegistry.getTargetContext(); 1859 sIsolatedContext = new IsolatedContext(sContext, "modern", /*asFuseThread*/ false); 1860 sIsolatedResolver = sIsolatedContext.getContentResolver(); 1861 sItemsProvider = new ItemsProvider(sIsolatedContext); 1862 } 1863 } 1864