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