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.extractRelativePathForDirectory; 21 import static com.android.providers.media.util.FileUtils.isDownload; 22 import static com.android.providers.media.util.FileUtils.isDownloadDir; 23 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.junit.Assert.assertArrayEquals; 27 import static org.junit.Assert.assertEquals; 28 import static org.junit.Assert.assertFalse; 29 import static org.junit.Assert.assertNotNull; 30 import static org.junit.Assert.assertNull; 31 import static org.junit.Assert.assertTrue; 32 import static org.junit.Assert.fail; 33 34 import android.Manifest; 35 import android.content.ContentProviderClient; 36 import android.content.ContentProviderOperation; 37 import android.content.ContentResolver; 38 import android.content.ContentUris; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.pm.PackageManager; 43 import android.database.Cursor; 44 import android.net.Uri; 45 import android.os.Build; 46 import android.os.Bundle; 47 import android.os.CancellationSignal; 48 import android.os.Environment; 49 import android.provider.MediaStore; 50 import android.provider.MediaStore.Images.ImageColumns; 51 import android.provider.MediaStore.MediaColumns; 52 import android.util.ArrayMap; 53 import android.util.Log; 54 import android.util.Pair; 55 56 import androidx.test.InstrumentationRegistry; 57 import androidx.test.runner.AndroidJUnit4; 58 59 import com.android.providers.media.MediaProvider.FallbackException; 60 import com.android.providers.media.MediaProvider.VolumeArgumentException; 61 import com.android.providers.media.MediaProvider.VolumeNotFoundException; 62 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext; 63 import com.android.providers.media.util.FileUtils; 64 import com.android.providers.media.util.SQLiteQueryBuilder; 65 66 import org.junit.AfterClass; 67 import org.junit.Assume; 68 import org.junit.BeforeClass; 69 import org.junit.Ignore; 70 import org.junit.Test; 71 import org.junit.runner.RunWith; 72 73 import java.io.ByteArrayOutputStream; 74 import java.io.File; 75 import java.io.PrintWriter; 76 import java.util.ArrayList; 77 import java.util.Arrays; 78 import java.util.Collection; 79 import java.util.Collections; 80 import java.util.Locale; 81 import java.util.regex.Pattern; 82 83 @RunWith(AndroidJUnit4.class) 84 public class MediaProviderTest { 85 static final String TAG = "MediaProviderTest"; 86 87 /** 88 * To confirm behaviors, we need to pick an app installed on all devices 89 * which has no permissions, and the best candidate is the "Easter Egg" app. 90 */ 91 static final String PERMISSIONLESS_APP = "com.android.egg"; 92 93 private static Context sIsolatedContext; 94 private static ContentResolver sIsolatedResolver; 95 96 @BeforeClass setUp()97 public static void setUp() { 98 InstrumentationRegistry.getInstrumentation().getUiAutomation() 99 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, 100 Manifest.permission.READ_COMPAT_CHANGE_CONFIG); 101 102 final Context context = InstrumentationRegistry.getTargetContext(); 103 sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); 104 sIsolatedResolver = sIsolatedContext.getContentResolver(); 105 } 106 107 @AfterClass tearDown()108 public static void tearDown() { 109 InstrumentationRegistry.getInstrumentation() 110 .getUiAutomation().dropShellPermissionIdentity(); 111 } 112 113 /** 114 * To fully exercise all our tests, we require that the Cuttlefish emulator 115 * have both emulated primary storage and an SD card be present. 116 */ 117 @Test testCuttlefish()118 public void testCuttlefish() { 119 Assume.assumeTrue(Build.MODEL.contains("Cuttlefish")); 120 121 assertTrue("Cuttlefish must have both emulated storage and an SD card to exercise tests", 122 MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext()) 123 .size() > 1); 124 } 125 126 @Test testSchema()127 public void testSchema() { 128 for (String path : new String[] { 129 "images/media", 130 "images/media/1", 131 "images/thumbnails", 132 "images/thumbnails/1", 133 134 "audio/media", 135 "audio/media/1", 136 "audio/media/1/genres", 137 "audio/media/1/genres/1", 138 "audio/genres", 139 "audio/genres/1", 140 "audio/genres/1/members", 141 "audio/playlists", 142 "audio/playlists/1", 143 "audio/playlists/1/members", 144 "audio/playlists/1/members/1", 145 "audio/artists", 146 "audio/artists/1", 147 "audio/artists/1/albums", 148 "audio/albums", 149 "audio/albums/1", 150 "audio/albumart", 151 "audio/albumart/1", 152 153 "video/media", 154 "video/media/1", 155 "video/thumbnails", 156 "video/thumbnails/1", 157 158 "file", 159 "file/1", 160 161 "downloads", 162 "downloads/1", 163 }) { 164 final Uri probe = MediaStore.AUTHORITY_URI.buildUpon() 165 .appendPath(MediaStore.VOLUME_EXTERNAL).appendEncodedPath(path).build(); 166 try (Cursor c = sIsolatedResolver.query(probe, null, null, null)) { 167 assertNotNull("probe", c); 168 } 169 try { 170 sIsolatedResolver.getType(probe); 171 } catch (IllegalStateException tolerated) { 172 } 173 } 174 } 175 176 @Test testLocale()177 public void testLocale() { 178 try (ContentProviderClient cpc = sIsolatedResolver 179 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 180 ((MediaProvider) cpc.getLocalContentProvider()) 181 .onLocaleChanged(); 182 } 183 } 184 185 @Test testDump()186 public void testDump() throws Exception { 187 try (ContentProviderClient cpc = sIsolatedResolver 188 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 189 cpc.getLocalContentProvider().dump(null, 190 new PrintWriter(new ByteArrayOutputStream()), null); 191 } 192 } 193 194 /** 195 * Verify that our fallback exceptions throw on modern apps while degrading 196 * gracefully for legacy apps. 197 */ 198 @Test testFallbackException()199 public void testFallbackException() throws Exception { 200 for (FallbackException e : new FallbackException[] { 201 new FallbackException("test", Build.VERSION_CODES.Q), 202 new VolumeNotFoundException("test"), 203 new VolumeArgumentException(new File("/"), Collections.emptyList()) 204 }) { 205 // Modern apps should get thrown 206 assertThrows(Exception.class, () -> { 207 e.translateForInsert(Build.VERSION_CODES.CUR_DEVELOPMENT); 208 }); 209 assertThrows(Exception.class, () -> { 210 e.translateForUpdateDelete(Build.VERSION_CODES.CUR_DEVELOPMENT); 211 }); 212 assertThrows(Exception.class, () -> { 213 e.translateForQuery(Build.VERSION_CODES.CUR_DEVELOPMENT); 214 }); 215 216 // Legacy apps gracefully log without throwing 217 assertEquals(null, e.translateForInsert(Build.VERSION_CODES.BASE)); 218 assertEquals(0, e.translateForUpdateDelete(Build.VERSION_CODES.BASE)); 219 assertEquals(null, e.translateForQuery(Build.VERSION_CODES.BASE)); 220 } 221 } 222 223 /** 224 * We already have solid coverage of this logic in {@link IdleServiceTest}, 225 * but the coverage system currently doesn't measure that, so we add the 226 * bare minimum local testing here to convince the tooling that it's 227 * covered. 228 */ 229 @Test testIdle()230 public void testIdle() throws Exception { 231 try (ContentProviderClient cpc = sIsolatedResolver 232 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 233 ((MediaProvider) cpc.getLocalContentProvider()) 234 .onIdleMaintenance(new CancellationSignal()); 235 } 236 } 237 238 /** 239 * We already have solid coverage of this logic in 240 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 241 * measure that, so we add the bare minimum local testing here to convince 242 * the tooling that it's covered. 243 */ 244 @Test testCanonicalize()245 public void testCanonicalize() throws Exception { 246 // We might have old files lurking, so force a clean slate 247 final Context context = InstrumentationRegistry.getTargetContext(); 248 sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); 249 sIsolatedResolver = sIsolatedContext.getContentResolver(); 250 251 final File dir = Environment 252 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 253 for (File file : new File[] { 254 stage(R.raw.test_audio, new File(dir, "test" + System.nanoTime() + ".mp3")), 255 stage(R.raw.test_video_xmp, new File(dir, "test" + System.nanoTime() + ".mp4")), 256 stage(R.raw.lg_g4_iso_800_jpg, new File(dir, "test" + System.nanoTime() + ".jpg")) 257 }) { 258 final Uri uri = MediaStore.scanFile(sIsolatedResolver, file); 259 Log.v(TAG, "Scanned " + file + " as " + uri); 260 261 final Uri forward = sIsolatedResolver.canonicalize(uri); 262 final Uri reverse = sIsolatedResolver.uncanonicalize(forward); 263 264 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(forward)); 265 assertEquals(ContentUris.parseId(uri), ContentUris.parseId(reverse)); 266 } 267 } 268 269 /** 270 * We already have solid coverage of this logic in 271 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 272 * measure that, so we add the bare minimum local testing here to convince 273 * the tooling that it's covered. 274 */ 275 @Test testMetadata()276 public void testMetadata() { 277 assertNotNull(MediaStore.getVersion(sIsolatedContext, 278 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 279 assertNotNull(MediaStore.getGeneration(sIsolatedResolver, 280 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 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 testCreateRequest()290 public void testCreateRequest() throws Exception { 291 final Collection<Uri> uris = Arrays.asList( 292 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)); 293 assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris)); 294 } 295 296 /** 297 * We already have solid coverage of this logic in 298 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 299 * measure that, so we add the bare minimum local testing here to convince 300 * the tooling that it's covered. 301 */ 302 @Test testCheckUriPermission()303 public void testCheckUriPermission() throws Exception { 304 final ContentValues values = new ContentValues(); 305 values.put(MediaColumns.DISPLAY_NAME, "test.mp3"); 306 values.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 307 final Uri uri = sIsolatedResolver.insert( 308 MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values); 309 310 assertEquals(PackageManager.PERMISSION_GRANTED, sIsolatedResolver.checkUriPermission(uri, 311 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)); 312 } 313 314 /** 315 * We already have solid coverage of this logic in 316 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 317 * measure that, so we add the bare minimum local testing here to convince 318 * the tooling that it's covered. 319 */ 320 @Test testBulkInsert()321 public void testBulkInsert() throws Exception { 322 final ContentValues values1 = new ContentValues(); 323 values1.put(MediaColumns.DISPLAY_NAME, "test1.mp3"); 324 values1.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 325 326 final ContentValues values2 = new ContentValues(); 327 values2.put(MediaColumns.DISPLAY_NAME, "test2.mp3"); 328 values2.put(MediaColumns.MIME_TYPE, "audio/mpeg"); 329 330 final Uri targetUri = MediaStore.Audio.Media 331 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 332 assertEquals(2, sIsolatedResolver.bulkInsert(targetUri, 333 new ContentValues[] { values1, values2 })); 334 } 335 336 /** 337 * We already have solid coverage of this logic in 338 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 339 * measure that, so we add the bare minimum local testing here to convince 340 * the tooling that it's covered. 341 */ 342 @Test testCustomCollator()343 public void testCustomCollator() throws Exception { 344 final Bundle extras = new Bundle(); 345 extras.putString(ContentResolver.QUERY_ARG_SORT_LOCALE, "en"); 346 347 try (Cursor c = sIsolatedResolver.query(MediaStore.Files.EXTERNAL_CONTENT_URI, 348 null, extras, null)) { 349 assertNotNull(c); 350 } 351 } 352 353 /** 354 * We already have solid coverage of this logic in 355 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 356 * measure that, so we add the bare minimum local testing here to convince 357 * the tooling that it's covered. 358 */ 359 @Test testGetRedactionRanges_Image()360 public void testGetRedactionRanges_Image() throws Exception { 361 final File file = File.createTempFile("test", ".jpg"); 362 stage(R.raw.test_image, file); 363 assertNotNull(MediaProvider.getRedactionRanges(file)); 364 } 365 366 /** 367 * We already have solid coverage of this logic in 368 * {@code CtsProviderTestCases}, but the coverage system currently doesn't 369 * measure that, so we add the bare minimum local testing here to convince 370 * the tooling that it's covered. 371 */ 372 @Test testGetRedactionRanges_Video()373 public void testGetRedactionRanges_Video() throws Exception { 374 final File file = File.createTempFile("test", ".mp4"); 375 stage(R.raw.test_video, file); 376 assertNotNull(MediaProvider.getRedactionRanges(file)); 377 } 378 379 @Test testComputeCommonPrefix_Single()380 public void testComputeCommonPrefix_Single() { 381 assertEquals(Uri.parse("content://authority/1/2/3"), 382 MediaProvider.computeCommonPrefix(Arrays.asList( 383 Uri.parse("content://authority/1/2/3")))); 384 } 385 386 @Test testComputeCommonPrefix_Deeper()387 public void testComputeCommonPrefix_Deeper() { 388 assertEquals(Uri.parse("content://authority/1/2/3"), 389 MediaProvider.computeCommonPrefix(Arrays.asList( 390 Uri.parse("content://authority/1/2/3/4"), 391 Uri.parse("content://authority/1/2/3/4/5"), 392 Uri.parse("content://authority/1/2/3")))); 393 } 394 395 @Test testComputeCommonPrefix_Siblings()396 public void testComputeCommonPrefix_Siblings() { 397 assertEquals(Uri.parse("content://authority/1/2"), 398 MediaProvider.computeCommonPrefix(Arrays.asList( 399 Uri.parse("content://authority/1/2/3"), 400 Uri.parse("content://authority/1/2/99")))); 401 } 402 403 @Test testComputeCommonPrefix_Drastic()404 public void testComputeCommonPrefix_Drastic() { 405 assertEquals(Uri.parse("content://authority"), 406 MediaProvider.computeCommonPrefix(Arrays.asList( 407 Uri.parse("content://authority/1/2/3"), 408 Uri.parse("content://authority/99/99/99")))); 409 } 410 getPathOwnerPackageName(String path)411 private static String getPathOwnerPackageName(String path) { 412 return FileUtils.extractPathOwnerPackageName(path); 413 } 414 415 @Test testPathOwnerPackageName_None()416 public void testPathOwnerPackageName_None() throws Exception { 417 assertEquals(null, getPathOwnerPackageName(null)); 418 assertEquals(null, getPathOwnerPackageName("/data/path")); 419 } 420 421 @Test testPathOwnerPackageName_Emulated()422 public void testPathOwnerPackageName_Emulated() throws Exception { 423 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/DCIM/foo.jpg")); 424 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/")); 425 assertEquals(null, getPathOwnerPackageName("/storage/emulated/0/Android/data/")); 426 427 assertEquals("com.example", 428 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/")); 429 assertEquals("com.example", 430 getPathOwnerPackageName("/storage/emulated/0/Android/data/com.example/foo.jpg")); 431 assertEquals("com.example", 432 getPathOwnerPackageName("/storage/emulated/0/Android/obb/com.example/foo.jpg")); 433 assertEquals("com.example", 434 getPathOwnerPackageName("/storage/emulated/0/Android/media/com.example/foo.jpg")); 435 assertEquals("com.example", 436 getPathOwnerPackageName("/storage/emulated/0/Android/sandbox/com.example/foo.jpg")); 437 } 438 439 @Test testPathOwnerPackageName_Portable()440 public void testPathOwnerPackageName_Portable() throws Exception { 441 assertEquals(null, getPathOwnerPackageName("/storage/0000-0000/DCIM/foo.jpg")); 442 443 assertEquals("com.example", 444 getPathOwnerPackageName("/storage/0000-0000/Android/data/com.example/foo.jpg")); 445 } 446 447 @Test testBuildData_Simple()448 public void testBuildData_Simple() throws Exception { 449 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 450 assertEndsWith("/Pictures/file.png", 451 buildFile(uri, null, "file", "image/png")); 452 assertEndsWith("/Pictures/file.png", 453 buildFile(uri, null, "file.png", "image/png")); 454 assertEndsWith("/Pictures/file.jpg.png", 455 buildFile(uri, null, "file.jpg", "image/png")); 456 } 457 458 @Test testBuildData_Primary()459 public void testBuildData_Primary() throws Exception { 460 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 461 assertEndsWith("/DCIM/IMG_1024.JPG", 462 buildFile(uri, Environment.DIRECTORY_DCIM, "IMG_1024.JPG", "image/jpeg")); 463 } 464 465 @Test 466 @Ignore("Enable as part of b/142561358") testBuildData_Secondary()467 public void testBuildData_Secondary() throws Exception { 468 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 469 assertEndsWith("/Pictures/Screenshots/foo.png", 470 buildFile(uri, "Pictures/Screenshots", "foo.png", "image/png")); 471 } 472 473 @Test testBuildData_InvalidNames()474 public void testBuildData_InvalidNames() throws Exception { 475 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 476 assertEndsWith("/Pictures/foo_bar.png", 477 buildFile(uri, null, "foo/bar", "image/png")); 478 assertEndsWith("/Pictures/_.hidden.png", 479 buildFile(uri, null, ".hidden", "image/png")); 480 } 481 482 @Test testBuildData_InvalidTypes()483 public void testBuildData_InvalidTypes() throws Exception { 484 for (String type : new String[] { 485 "audio/foo", "video/foo", "image/foo", "application/foo", "foo/foo" 486 }) { 487 if (!type.startsWith("audio/")) { 488 assertThrows(IllegalArgumentException.class, () -> { 489 buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 490 null, "foo", type); 491 }); 492 } 493 if (!type.startsWith("video/")) { 494 assertThrows(IllegalArgumentException.class, () -> { 495 buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 496 null, "foo", type); 497 }); 498 } 499 if (!type.startsWith("image/")) { 500 assertThrows(IllegalArgumentException.class, () -> { 501 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 502 null, "foo", type); 503 }); 504 } 505 } 506 } 507 508 @Test testBuildData_InvalidSecondaryTypes()509 public void testBuildData_InvalidSecondaryTypes() throws Exception { 510 assertEndsWith("/Pictures/foo.png", 511 buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 512 null, "foo.png", "image/*")); 513 514 assertThrows(IllegalArgumentException.class, () -> { 515 buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 516 null, "foo", "video/*"); 517 }); 518 assertThrows(IllegalArgumentException.class, () -> { 519 buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 520 null, "foo.mp4", "audio/*"); 521 }); 522 } 523 524 @Test testBuildData_EmptyTypes()525 public void testBuildData_EmptyTypes() throws Exception { 526 Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 527 assertEndsWith("/Pictures/foo.png", 528 buildFile(uri, null, "foo.png", "")); 529 530 uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 531 assertEndsWith(".mp4", 532 buildFile(uri, null, "", "")); 533 } 534 535 @Test testEnsureFileColumns_InvalidMimeType_targetSdkQ()536 public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception { 537 final MediaProvider provider = new MediaProvider() { 538 @Override 539 public boolean isFuseThread() { 540 return false; 541 } 542 543 @Override 544 public int getCallingPackageTargetSdkVersion() { 545 return Build.VERSION_CODES.Q; 546 } 547 }; 548 549 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 550 final ContentValues values = new ContentValues(); 551 552 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 553 provider.ensureFileColumns(uri, values); 554 assertMimetype(values, "image/jpeg"); 555 assertDisplayName(values, "pngimage.png.jpg"); 556 557 values.clear(); 558 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 559 values.put(MediaColumns.MIME_TYPE, ""); 560 provider.ensureFileColumns(uri, values); 561 assertMimetype(values, "image/jpeg"); 562 assertDisplayName(values, "pngimage.png.jpg"); 563 564 values.clear(); 565 values.put(MediaColumns.MIME_TYPE, ""); 566 provider.ensureFileColumns(uri, values); 567 assertMimetype(values, "image/jpeg"); 568 569 values.clear(); 570 values.put(MediaColumns.DISPLAY_NAME, "foo.foo"); 571 provider.ensureFileColumns(uri, values); 572 assertMimetype(values, "image/jpeg"); 573 assertDisplayName(values, "foo.foo.jpg"); 574 } 575 576 @Ignore("Enable as part of b/142561358") testBuildData_Charset()577 public void testBuildData_Charset() throws Exception { 578 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 579 assertEndsWith("/Pictures/foo__bar/bar__baz.png", 580 buildFile(uri, "Pictures/foo\0\0bar", "bar::baz.png", "image/png")); 581 } 582 583 @Test testBuildData_Playlists()584 public void testBuildData_Playlists() throws Exception { 585 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 586 assertEndsWith("/Music/my_playlist.m3u", 587 buildFile(uri, null, "my_playlist", "audio/mpegurl")); 588 assertEndsWith("/Movies/my_playlist.pls", 589 buildFile(uri, "Movies", "my_playlist", "audio/x-scpls")); 590 } 591 592 @Test testBuildData_Subtitles()593 public void testBuildData_Subtitles() throws Exception { 594 final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 595 assertEndsWith("/Movies/my_subtitle.srt", 596 buildFile(uri, null, "my_subtitle", "application/x-subrip")); 597 assertEndsWith("/Music/my_lyrics.lrc", 598 buildFile(uri, "Music", "my_lyrics", "application/lrc")); 599 } 600 601 @Test testBuildData_Downloads()602 public void testBuildData_Downloads() throws Exception { 603 final Uri uri = MediaStore.Downloads 604 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 605 assertEndsWith("/Download/linux.iso", 606 buildFile(uri, null, "linux.iso", "application/x-iso9660-image")); 607 } 608 609 @Test testBuildData_Pending_FromValues()610 public void testBuildData_Pending_FromValues() throws Exception { 611 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 612 final ContentValues forward = new ContentValues(); 613 forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); 614 forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); 615 forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); 616 forward.put(MediaColumns.IS_PENDING, 1); 617 forward.put(MediaColumns.IS_TRASHED, 0); 618 forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); 619 ensureFileColumns(uri, forward); 620 621 // Requested filename remains intact, but raw path on disk is mutated to 622 // reflect that it's a pending item with a specific expiration time 623 assertEquals("IMG1024.JPG", 624 forward.getAsString(MediaColumns.DISPLAY_NAME)); 625 assertEndsWith(".pending-1577836800-IMG1024.JPG", 626 forward.getAsString(MediaColumns.DATA)); 627 } 628 629 @Test testBuildData_Pending_FromData()630 public void testBuildData_Pending_FromData() throws Exception { 631 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 632 final ContentValues reverse = new ContentValues(); 633 reverse.put(MediaColumns.DATA, 634 "/storage/emulated/0/DCIM/My Vacation/.pending-1577836800-IMG1024.JPG"); 635 ensureFileColumns(uri, reverse); 636 637 assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); 638 assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); 639 assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); 640 assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); 641 assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); 642 assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); 643 } 644 645 @Test testBuildData_Trashed_FromValues()646 public void testBuildData_Trashed_FromValues() throws Exception { 647 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 648 final ContentValues forward = new ContentValues(); 649 forward.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); 650 forward.put(MediaColumns.DISPLAY_NAME, "IMG1024.JPG"); 651 forward.put(MediaColumns.MIME_TYPE, "image/jpeg"); 652 forward.put(MediaColumns.IS_PENDING, 0); 653 forward.put(MediaColumns.IS_TRASHED, 1); 654 forward.put(MediaColumns.DATE_EXPIRES, 1577836800L); 655 ensureFileColumns(uri, forward); 656 657 // Requested filename remains intact, but raw path on disk is mutated to 658 // reflect that it's a trashed item with a specific expiration time 659 assertEquals("IMG1024.JPG", 660 forward.getAsString(MediaColumns.DISPLAY_NAME)); 661 assertEndsWith(".trashed-1577836800-IMG1024.JPG", 662 forward.getAsString(MediaColumns.DATA)); 663 } 664 665 @Test testBuildData_Trashed_FromData()666 public void testBuildData_Trashed_FromData() throws Exception { 667 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 668 final ContentValues reverse = new ContentValues(); 669 reverse.put(MediaColumns.DATA, 670 "/storage/emulated/0/DCIM/My Vacation/.trashed-1577836800-IMG1024.JPG"); 671 ensureFileColumns(uri, reverse); 672 673 assertEquals("DCIM/My Vacation/", reverse.getAsString(MediaColumns.RELATIVE_PATH)); 674 assertEquals("IMG1024.JPG", reverse.getAsString(MediaColumns.DISPLAY_NAME)); 675 assertEquals("image/jpeg", reverse.getAsString(MediaColumns.MIME_TYPE)); 676 assertEquals(0, (int) reverse.getAsInteger(MediaColumns.IS_PENDING)); 677 assertEquals(1, (int) reverse.getAsInteger(MediaColumns.IS_TRASHED)); 678 assertEquals(1577836800, (long) reverse.getAsLong(MediaColumns.DATE_EXPIRES)); 679 } 680 681 @Test testGreylist()682 public void testGreylist() throws Exception { 683 assertFalse(isGreylistMatch( 684 "SELECT secret FROM other_table")); 685 686 assertTrue(isGreylistMatch( 687 "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")); 688 assertTrue(isGreylistMatch( 689 "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)")); 690 assertTrue(isGreylistMatch( 691 "0 as orientation")); 692 assertTrue(isGreylistMatch( 693 "\"content://media/internal/audio/media\"")); 694 } 695 696 @Test testGreylist_115845887()697 public void testGreylist_115845887() { 698 assertTrue(isGreylistMatch( 699 "MAX(*)")); 700 assertTrue(isGreylistMatch( 701 "MAX(_id)")); 702 703 assertTrue(isGreylistMatch( 704 "sum(column_name)")); 705 assertFalse(isGreylistMatch( 706 "SUM(foo+bar)")); 707 708 assertTrue(isGreylistMatch( 709 "count(column_name)")); 710 assertFalse(isGreylistMatch( 711 "count(other_table.column_name)")); 712 } 713 714 @Test testGreylist_116489751_116135586_116117120_116084561_116074030_116062802()715 public void testGreylist_116489751_116135586_116117120_116084561_116074030_116062802() { 716 assertTrue(isGreylistMatch( 717 "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)")); 718 } 719 720 @Test testGreylist_116699470()721 public void testGreylist_116699470() { 722 assertTrue(isGreylistMatch( 723 "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)")); 724 } 725 726 @Test testGreylist_116531759()727 public void testGreylist_116531759() { 728 assertTrue(isGreylistMatch( 729 "count(*)")); 730 assertTrue(isGreylistMatch( 731 "COUNT(*)")); 732 assertFalse(isGreylistMatch( 733 "xCOUNT(*)")); 734 assertTrue(isGreylistMatch( 735 "count(*) AS image_count")); 736 assertTrue(isGreylistMatch( 737 "count(_id)")); 738 assertTrue(isGreylistMatch( 739 "count(_id) AS image_count")); 740 741 assertTrue(isGreylistMatch( 742 "column_a AS column_b")); 743 assertFalse(isGreylistMatch( 744 "other_table.column_a AS column_b")); 745 } 746 747 @Test testGreylist_118475754()748 public void testGreylist_118475754() { 749 assertTrue(isGreylistMatch( 750 "count(*) pcount")); 751 assertTrue(isGreylistMatch( 752 "foo AS bar")); 753 assertTrue(isGreylistMatch( 754 "foo bar")); 755 assertTrue(isGreylistMatch( 756 "count(foo) AS bar")); 757 assertTrue(isGreylistMatch( 758 "count(foo) bar")); 759 } 760 761 @Test testGreylist_119522660()762 public void testGreylist_119522660() { 763 assertTrue(isGreylistMatch( 764 "CAST(_id AS TEXT) AS string_id")); 765 assertTrue(isGreylistMatch( 766 "cast(_id as text)")); 767 } 768 769 @Test testGreylist_126945991()770 public void testGreylist_126945991() { 771 assertTrue(isGreylistMatch( 772 "substr(_data, length(_data)-length(_display_name), 1) as filename_prevchar")); 773 } 774 775 @Test testGreylist_127900881()776 public void testGreylist_127900881() { 777 assertTrue(isGreylistMatch( 778 "*")); 779 } 780 781 @Test testGreylist_128389972()782 public void testGreylist_128389972() { 783 assertTrue(isGreylistMatch( 784 " count(bucket_id) images_count")); 785 } 786 787 @Test testGreylist_129746861()788 public void testGreylist_129746861() { 789 assertTrue(isGreylistMatch( 790 "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")); 791 } 792 793 @Test testGreylist_114112523()794 public void testGreylist_114112523() { 795 assertTrue(isGreylistMatch( 796 "audio._id AS _id")); 797 } 798 799 @Test testComputeProjection_AggregationAllowed()800 public void testComputeProjection_AggregationAllowed() throws Exception { 801 final SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); 802 final ArrayMap<String, String> map = new ArrayMap<>(); 803 map.put("external", "internal"); 804 builder.setProjectionMap(map); 805 builder.setStrict(true); 806 807 assertArrayEquals( 808 new String[] { "internal" }, 809 builder.computeProjection(null)); 810 assertArrayEquals( 811 new String[] { "internal" }, 812 builder.computeProjection(new String[] { "external" })); 813 assertThrows(IllegalArgumentException.class, () -> { 814 builder.computeProjection(new String[] { "internal" }); 815 }); 816 assertThrows(IllegalArgumentException.class, () -> { 817 builder.computeProjection(new String[] { "MIN(internal)" }); 818 }); 819 assertArrayEquals( 820 new String[] { "MIN(internal)" }, 821 builder.computeProjection(new String[] { "MIN(external)" })); 822 assertThrows(IllegalArgumentException.class, () -> { 823 builder.computeProjection(new String[] { "FOO(external)" }); 824 }); 825 } 826 827 @Test testIsDownload()828 public void testIsDownload() throws Exception { 829 assertTrue(isDownload("/storage/emulated/0/Download/colors.png")); 830 assertTrue(isDownload("/storage/emulated/0/Download/test.pdf")); 831 assertTrue(isDownload("/storage/emulated/0/Download/dir/foo.mp4")); 832 assertTrue(isDownload("/storage/0000-0000/Download/foo.txt")); 833 assertTrue(isDownload( 834 "/storage/emulated/0/Android/sandbox/com.example/Download/colors.png")); 835 assertTrue(isDownload( 836 "/storage/emulated/0/Android/sandbox/shared-com.uid.shared/Download/colors.png")); 837 assertTrue(isDownload( 838 "/storage/0000-0000/Android/sandbox/com.example/Download/colors.png")); 839 assertTrue(isDownload( 840 "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download/colors.png")); 841 842 843 assertFalse(isDownload("/storage/emulated/0/Pictures/colors.png")); 844 assertFalse(isDownload("/storage/emulated/0/Pictures/Download/colors.png")); 845 assertFalse(isDownload("/storage/emulated/0/Android/data/com.example/Download/foo.txt")); 846 assertFalse(isDownload( 847 "/storage/emulated/0/Android/sandbox/com.example/dir/Download/foo.txt")); 848 assertFalse(isDownload("/storage/emulated/0/Download")); 849 assertFalse(isDownload("/storage/emulated/0/Android/sandbox/com.example/Download")); 850 assertFalse(isDownload( 851 "/storage/0000-0000/Android/sandbox/shared-com.uid.shared/Download")); 852 } 853 854 @Test testIsDownloadDir()855 public void testIsDownloadDir() throws Exception { 856 assertTrue(isDownloadDir("/storage/emulated/0/Download")); 857 assertTrue(isDownloadDir("/storage/emulated/0/Android/sandbox/com.example/Download")); 858 859 assertFalse(isDownloadDir("/storage/emulated/0/Download/colors.png")); 860 assertFalse(isDownloadDir("/storage/emulated/0/Download/dir/")); 861 assertFalse(isDownloadDir( 862 "/storage/emulated/0/Android/sandbox/com.example/Download/dir/foo.txt")); 863 } 864 865 @Test testComputeDataValues_Grouped()866 public void testComputeDataValues_Grouped() throws Exception { 867 for (String data : new String[] { 868 "/storage/0000-0000/DCIM/Camera/IMG1024.JPG", 869 "/storage/0000-0000/DCIM/Camera/iMg1024.JpG", 870 "/storage/0000-0000/DCIM/Camera/IMG1024.CR2", 871 "/storage/0000-0000/DCIM/Camera/IMG1024.BURST001.JPG", 872 }) { 873 final ContentValues values = computeDataValues(data); 874 assertVolume(values, "0000-0000"); 875 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 876 assertRelativePath(values, "DCIM/Camera/"); 877 } 878 } 879 880 @Test testComputeDataValues_Extensions()881 public void testComputeDataValues_Extensions() throws Exception { 882 ContentValues values; 883 884 values = computeDataValues("/storage/0000-0000/DCIM/Camera/IMG1024"); 885 assertVolume(values, "0000-0000"); 886 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 887 assertRelativePath(values, "DCIM/Camera/"); 888 889 values = computeDataValues("/storage/0000-0000/DCIM/Camera/.foo"); 890 assertVolume(values, "0000-0000"); 891 assertBucket(values, "/storage/0000-0000/DCIM/Camera", "Camera"); 892 assertRelativePath(values, "DCIM/Camera/"); 893 894 values = computeDataValues("/storage/476A-17F8/123456/test.png"); 895 assertVolume(values, "476a-17f8"); 896 assertBucket(values, "/storage/476A-17F8/123456", "123456"); 897 assertRelativePath(values, "123456/"); 898 899 values = computeDataValues("/storage/476A-17F8/123456/789/test.mp3"); 900 assertVolume(values, "476a-17f8"); 901 assertBucket(values, "/storage/476A-17F8/123456/789", "789"); 902 assertRelativePath(values, "123456/789/"); 903 } 904 905 @Test testComputeDataValues_DirectoriesInvalid()906 public void testComputeDataValues_DirectoriesInvalid() throws Exception { 907 for (String data : new String[] { 908 "/storage/IMG1024.JPG", 909 "/data/media/IMG1024.JPG", 910 "IMG1024.JPG", 911 }) { 912 final ContentValues values = computeDataValues(data); 913 assertRelativePath(values, null); 914 } 915 } 916 917 @Test testComputeDataValues_Directories()918 public void testComputeDataValues_Directories() throws Exception { 919 ContentValues values; 920 921 for (String top : new String[] { 922 "/storage/emulated/0", 923 "/storage/emulated/0/Android/sandbox/com.example", 924 }) { 925 values = computeDataValues(top + "/IMG1024.JPG"); 926 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 927 assertBucket(values, top, null); 928 assertRelativePath(values, "/"); 929 930 values = computeDataValues(top + "/One/IMG1024.JPG"); 931 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 932 assertBucket(values, top + "/One", "One"); 933 assertRelativePath(values, "One/"); 934 935 values = computeDataValues(top + "/One/Two/IMG1024.JPG"); 936 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 937 assertBucket(values, top + "/One/Two", "Two"); 938 assertRelativePath(values, "One/Two/"); 939 940 values = computeDataValues(top + "/One/Two/Three/IMG1024.JPG"); 941 assertVolume(values, MediaStore.VOLUME_EXTERNAL_PRIMARY); 942 assertBucket(values, top + "/One/Two/Three", "Three"); 943 assertRelativePath(values, "One/Two/Three/"); 944 } 945 } 946 947 @Test testEnsureFileColumns_resolvesMimeType()948 public void testEnsureFileColumns_resolvesMimeType() throws Exception { 949 final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 950 final ContentValues values = new ContentValues(); 951 values.put(MediaColumns.DISPLAY_NAME, "pngimage.png"); 952 953 final MediaProvider provider = new MediaProvider() { 954 @Override 955 public boolean isFuseThread() { 956 return false; 957 } 958 959 @Override 960 public int getCallingPackageTargetSdkVersion() { 961 return Build.VERSION_CODES.CUR_DEVELOPMENT; 962 } 963 }; 964 provider.ensureFileColumns(uri, values); 965 966 assertMimetype(values, "image/png"); 967 } 968 969 @Test testRelativePathForInvalidDirectories()970 public void testRelativePathForInvalidDirectories() throws Exception { 971 for (String path : new String[] { 972 "/storage/emulated", 973 "/storage", 974 "/data/media/Foo.jpg", 975 "Foo.jpg", 976 "storage/Foo" 977 }) { 978 assertEquals(null, FileUtils.extractRelativePathForDirectory(path)); 979 } 980 } 981 982 @Test testRelativePathForValidDirectories()983 public void testRelativePathForValidDirectories() throws Exception { 984 for (String prefix : new String[] { 985 "/storage/emulated/0", 986 "/storage/emulated/10", 987 "/storage/ABCD-1234" 988 }) { 989 assertRelativePathForDirectory(prefix, "/"); 990 assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/"); 991 assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/"); 992 assertRelativePathForDirectory(prefix + "/Z", "Z/"); 993 assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo", 994 "Android/media/com.example/Foo/"); 995 } 996 } 997 assertRelativePathForDirectory(String directoryPath, String relativePath)998 private static void assertRelativePathForDirectory(String directoryPath, String relativePath) { 999 assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :") 1000 .that(extractRelativePathForDirectory(directoryPath)) 1001 .isEqualTo(relativePath); 1002 } 1003 computeDataValues(String path)1004 private static ContentValues computeDataValues(String path) { 1005 final ContentValues values = new ContentValues(); 1006 values.put(MediaColumns.DATA, path); 1007 FileUtils.computeValuesFromData(values, /*forFuse*/ false); 1008 Log.v(TAG, "Computed values " + values); 1009 return values; 1010 } 1011 assertBucket(ContentValues values, String bucketId, String bucketName)1012 private static void assertBucket(ContentValues values, String bucketId, String bucketName) { 1013 if (bucketId != null) { 1014 assertEquals(bucketName, 1015 values.getAsString(ImageColumns.BUCKET_DISPLAY_NAME)); 1016 assertEquals(bucketId.toLowerCase(Locale.ROOT).hashCode(), 1017 (long) values.getAsLong(ImageColumns.BUCKET_ID)); 1018 } else { 1019 assertNull(values.get(ImageColumns.BUCKET_DISPLAY_NAME)); 1020 assertNull(values.get(ImageColumns.BUCKET_ID)); 1021 } 1022 } 1023 assertVolume(ContentValues values, String volumeName)1024 private static void assertVolume(ContentValues values, String volumeName) { 1025 assertEquals(volumeName, values.getAsString(ImageColumns.VOLUME_NAME)); 1026 } 1027 assertRelativePath(ContentValues values, String relativePath)1028 private static void assertRelativePath(ContentValues values, String relativePath) { 1029 assertEquals(relativePath, values.get(ImageColumns.RELATIVE_PATH)); 1030 } 1031 assertMimetype(ContentValues values, String type)1032 private static void assertMimetype(ContentValues values, String type) { 1033 assertEquals(type, values.get(MediaColumns.MIME_TYPE)); 1034 } 1035 assertDisplayName(ContentValues values, String type)1036 private static void assertDisplayName(ContentValues values, String type) { 1037 assertEquals(type, values.get(MediaColumns.DISPLAY_NAME)); 1038 } 1039 isGreylistMatch(String raw)1040 private static boolean isGreylistMatch(String raw) { 1041 for (Pattern p : MediaProvider.sGreylist) { 1042 if (p.matcher(raw).matches()) { 1043 return true; 1044 } 1045 } 1046 return false; 1047 } 1048 buildFile(Uri uri, String relativePath, String displayName, String mimeType)1049 private String buildFile(Uri uri, String relativePath, String displayName, 1050 String mimeType) { 1051 final ContentValues values = new ContentValues(); 1052 if (relativePath != null) { 1053 values.put(MediaColumns.RELATIVE_PATH, relativePath); 1054 } 1055 values.put(MediaColumns.DISPLAY_NAME, displayName); 1056 values.put(MediaColumns.MIME_TYPE, mimeType); 1057 try { 1058 ensureFileColumns(uri, values); 1059 } catch (VolumeArgumentException | VolumeNotFoundException e) { 1060 throw e.rethrowAsIllegalArgumentException(); 1061 } 1062 return values.getAsString(MediaColumns.DATA); 1063 } 1064 ensureFileColumns(Uri uri, ContentValues values)1065 private void ensureFileColumns(Uri uri, ContentValues values) 1066 throws VolumeArgumentException, VolumeNotFoundException { 1067 try (ContentProviderClient cpc = sIsolatedResolver 1068 .acquireContentProviderClient(MediaStore.AUTHORITY)) { 1069 ((MediaProvider) cpc.getLocalContentProvider()) 1070 .ensureFileColumns(uri, values); 1071 } 1072 } 1073 assertEndsWith(String expected, String actual)1074 private static void assertEndsWith(String expected, String actual) { 1075 if (!actual.endsWith(expected)) { 1076 fail("Expected ends with " + expected + " but found " + actual); 1077 } 1078 } 1079 assertThrows(Class<T> clazz, Runnable r)1080 private static <T extends Exception> void assertThrows(Class<T> clazz, Runnable r) { 1081 try { 1082 r.run(); 1083 fail("Expected " + clazz + " to be thrown"); 1084 } catch (Exception e) { 1085 if (!clazz.isAssignableFrom(e.getClass())) { 1086 throw e; 1087 } 1088 } 1089 } 1090 1091 @Test testNestedTransaction_applyBatch()1092 public void testNestedTransaction_applyBatch() throws Exception { 1093 final Uri[] uris = new Uri[] { 1094 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, 0), 1095 MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 0), 1096 }; 1097 final ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 1098 ops.add(ContentProviderOperation.newDelete(uris[0]).build()); 1099 ops.add(ContentProviderOperation.newDelete(uris[1]).build()); 1100 sIsolatedResolver.applyBatch(MediaStore.AUTHORITY, ops); 1101 } 1102 } 1103