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