1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media.scan; 18 19 import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN; 20 import static com.android.providers.media.scan.MediaScannerTest.stage; 21 import static com.android.providers.media.scan.ModernMediaScanner.shouldScanPathAndIsPathHidden; 22 import static com.android.providers.media.scan.ModernMediaScanner.isFileAlbumArt; 23 import static com.android.providers.media.scan.ModernMediaScanner.parseOptional; 24 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalDate; 25 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalDateTaken; 26 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalImageResolution; 27 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalMimeType; 28 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalNumerator; 29 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalOrZero; 30 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalOrientation; 31 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalResolution; 32 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalTrack; 33 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalVideoResolution; 34 import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalYear; 35 import static com.android.providers.media.scan.ModernMediaScanner.shouldScanDirectory; 36 import static com.android.providers.media.util.FileUtils.isDirectoryHidden; 37 import static com.android.providers.media.util.FileUtils.isFileHidden; 38 39 import static org.junit.Assert.assertEquals; 40 import static org.junit.Assert.assertFalse; 41 import static org.junit.Assert.assertNotNull; 42 import static org.junit.Assert.assertTrue; 43 import static org.mockito.ArgumentMatchers.eq; 44 import static org.mockito.Mockito.mock; 45 import static org.mockito.Mockito.when; 46 47 import android.Manifest; 48 import android.content.ContentResolver; 49 import android.content.ContentUris; 50 import android.content.Context; 51 import android.database.Cursor; 52 import android.graphics.Bitmap; 53 import android.media.ExifInterface; 54 import android.media.MediaMetadataRetriever; 55 import android.net.Uri; 56 import android.os.ParcelFileDescriptor; 57 import android.provider.MediaStore; 58 import android.provider.MediaStore.Files.FileColumns; 59 import android.provider.MediaStore.MediaColumns; 60 import android.util.Pair; 61 62 import androidx.test.InstrumentationRegistry; 63 import androidx.test.runner.AndroidJUnit4; 64 65 import com.android.providers.media.R; 66 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext; 67 import com.android.providers.media.util.FileUtils; 68 69 import org.junit.After; 70 import org.junit.Before; 71 import org.junit.Test; 72 import org.junit.runner.RunWith; 73 74 import java.io.File; 75 import java.io.FileOutputStream; 76 import java.util.Optional; 77 78 @RunWith(AndroidJUnit4.class) 79 public class ModernMediaScannerTest { 80 // TODO: scan directory-vs-files and confirm identical results 81 82 private File mDir; 83 84 private Context mIsolatedContext; 85 private ContentResolver mIsolatedResolver; 86 87 private ModernMediaScanner mModern; 88 89 @Before setUp()90 public void setUp() { 91 final Context context = InstrumentationRegistry.getTargetContext(); 92 InstrumentationRegistry.getInstrumentation().getUiAutomation() 93 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, 94 Manifest.permission.READ_COMPAT_CHANGE_CONFIG); 95 96 mDir = new File(context.getExternalMediaDirs()[0], "test_" + System.nanoTime()); 97 mDir.mkdirs(); 98 FileUtils.deleteContents(mDir); 99 100 mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); 101 mIsolatedResolver = mIsolatedContext.getContentResolver(); 102 103 mModern = new ModernMediaScanner(mIsolatedContext); 104 } 105 106 @After tearDown()107 public void tearDown() { 108 FileUtils.deleteContents(mDir); 109 InstrumentationRegistry.getInstrumentation() 110 .getUiAutomation().dropShellPermissionIdentity(); 111 } 112 113 @Test testSimple()114 public void testSimple() throws Exception { 115 assertNotNull(mModern.getContext()); 116 } 117 118 @Test testOverrideMimeType()119 public void testOverrideMimeType() throws Exception { 120 assertFalse(parseOptionalMimeType("image/png", null).isPresent()); 121 assertFalse(parseOptionalMimeType("image/png", "image").isPresent()); 122 assertFalse(parseOptionalMimeType("image/png", "im/im").isPresent()); 123 assertFalse(parseOptionalMimeType("image/png", "audio/x-shiny").isPresent()); 124 125 assertTrue(parseOptionalMimeType("image/png", "image/x-shiny").isPresent()); 126 assertEquals("image/x-shiny", 127 parseOptionalMimeType("image/png", "image/x-shiny").get()); 128 } 129 130 @Test testOverrideMimeType_148316354()131 public void testOverrideMimeType_148316354() throws Exception { 132 // Radical file type shifting isn't allowed 133 assertEquals(Optional.empty(), 134 parseOptionalMimeType("video/mp4", "audio/mpeg")); 135 136 // One specific narrow type of shift (mp4 -> m4a) is allowed 137 assertEquals(Optional.of("audio/mp4"), 138 parseOptionalMimeType("video/mp4", "audio/mp4")); 139 140 // The other direction isn't allowed 141 assertEquals(Optional.empty(), 142 parseOptionalMimeType("audio/mp4", "video/mp4")); 143 } 144 145 @Test testParseOptional()146 public void testParseOptional() throws Exception { 147 assertFalse(parseOptional(null).isPresent()); 148 assertFalse(parseOptional("").isPresent()); 149 assertFalse(parseOptional(" ").isPresent()); 150 assertFalse(parseOptional("-1").isPresent()); 151 152 assertFalse(parseOptional(-1).isPresent()); 153 assertTrue(parseOptional(0).isPresent()); 154 assertTrue(parseOptional(1).isPresent()); 155 156 assertEquals("meow", parseOptional("meow").get()); 157 assertEquals(42, (int) parseOptional(42).get()); 158 } 159 160 @Test testParseOptionalOrZero()161 public void testParseOptionalOrZero() throws Exception { 162 assertFalse(parseOptionalOrZero(-1).isPresent()); 163 assertFalse(parseOptionalOrZero(0).isPresent()); 164 assertTrue(parseOptionalOrZero(1).isPresent()); 165 } 166 167 @Test testParseOptionalNumerator()168 public void testParseOptionalNumerator() throws Exception { 169 assertEquals(12, (int) parseOptionalNumerator("12").get()); 170 assertEquals(12, (int) parseOptionalNumerator("12/24").get()); 171 172 assertFalse(parseOptionalNumerator("/24").isPresent()); 173 } 174 175 @Test testParseOptionalOrientation()176 public void testParseOptionalOrientation() throws Exception { 177 assertEquals(0, 178 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_NORMAL).get()); 179 assertEquals(90, 180 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_90).get()); 181 assertEquals(180, 182 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_180).get()); 183 assertEquals(270, 184 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_270).get()); 185 186 // We can't represent this as an orientation 187 assertFalse(parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).isPresent()); 188 } 189 190 @Test testParseOptionalImageResolution()191 public void testParseOptionalImageResolution() throws Exception { 192 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 193 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH))) 194 .thenReturn("640"); 195 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT))) 196 .thenReturn("480"); 197 assertEquals("640\u00d7480", parseOptionalImageResolution(mmr).get()); 198 } 199 200 @Test testParseOptionalVideoResolution()201 public void testParseOptionalVideoResolution() throws Exception { 202 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 203 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH))) 204 .thenReturn("640"); 205 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT))) 206 .thenReturn("480"); 207 assertEquals("640\u00d7480", parseOptionalVideoResolution(mmr).get()); 208 } 209 210 @Test testParseOptionalResolution()211 public void testParseOptionalResolution() throws Exception { 212 final ExifInterface exif = mock(ExifInterface.class); 213 when(exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).thenReturn("640"); 214 when(exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).thenReturn("480"); 215 assertEquals("640\u00d7480", parseOptionalResolution(exif).get()); 216 } 217 218 @Test testParseOptionalDate()219 public void testParseOptionalDate() throws Exception { 220 assertEquals(1577836800000L, (long) parseOptionalDate("20200101T000000").get()); 221 } 222 223 @Test testParseOptionalTrack()224 public void testParseOptionalTrack() throws Exception { 225 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 226 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER))) 227 .thenReturn("1/2"); 228 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER))) 229 .thenReturn("4/12"); 230 assertEquals(1004, (int) parseOptionalTrack(mmr).get()); 231 } 232 233 @Test testParseDateTaken_Complete()234 public void testParseDateTaken_Complete() throws Exception { 235 final File file = File.createTempFile("test", ".jpg"); 236 final ExifInterface exif = new ExifInterface(file); 237 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 238 239 // Offset is recorded, test both zeros 240 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-00:00"); 241 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 242 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00"); 243 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 244 245 // Offset is recorded, test both directions 246 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-07:00"); 247 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 248 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+07:00"); 249 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 250 } 251 252 @Test testParseDateTaken_Gps()253 public void testParseDateTaken_Gps() throws Exception { 254 final File file = File.createTempFile("test", ".jpg"); 255 final ExifInterface exif = new ExifInterface(file); 256 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 257 258 // GPS tells us we're in UTC 259 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 260 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:14:00"); 261 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 262 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 263 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:20:00"); 264 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 265 266 // GPS tells us we're in -7 267 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 268 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:14:00"); 269 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 270 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 271 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:20:00"); 272 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 273 274 // GPS tells us we're in +7 275 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 276 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:14:00"); 277 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 278 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 279 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:20:00"); 280 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 281 282 // GPS beyond 24 hours isn't helpful 283 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:27"); 284 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34"); 285 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 286 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:29"); 287 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34"); 288 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 289 } 290 291 @Test testParseDateTaken_File()292 public void testParseDateTaken_File() throws Exception { 293 final File file = File.createTempFile("test", ".jpg"); 294 final ExifInterface exif = new ExifInterface(file); 295 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 296 297 // Modified tells us we're in UTC 298 assertEquals(1453972654000L, 299 (long) parseOptionalDateTaken(exif, 1453972654000L - 60000L).get()); 300 assertEquals(1453972654000L, 301 (long) parseOptionalDateTaken(exif, 1453972654000L + 60000L).get()); 302 303 // Modified tells us we're in -7 304 assertEquals(1453972654000L + 25200000L, 305 (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L - 60000L).get()); 306 assertEquals(1453972654000L + 25200000L, 307 (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L + 60000L).get()); 308 309 // Modified tells us we're in +7 310 assertEquals(1453972654000L - 25200000L, 311 (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L - 60000L).get()); 312 assertEquals(1453972654000L - 25200000L, 313 (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L + 60000L).get()); 314 315 // Modified beyond 24 hours isn't helpful 316 assertFalse(parseOptionalDateTaken(exif, 1453972654000L - 86400000L).isPresent()); 317 assertFalse(parseOptionalDateTaken(exif, 1453972654000L + 86400000L).isPresent()); 318 } 319 320 @Test testParseDateTaken_Hopeless()321 public void testParseDateTaken_Hopeless() throws Exception { 322 final File file = File.createTempFile("test", ".jpg"); 323 final ExifInterface exif = new ExifInterface(file); 324 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 325 326 // Offset is completely missing, and no useful GPS or modified time 327 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 328 } 329 330 @Test testParseYear_Invalid()331 public void testParseYear_Invalid() throws Exception { 332 assertEquals(Optional.empty(), parseOptionalYear(null)); 333 assertEquals(Optional.empty(), parseOptionalYear("")); 334 assertEquals(Optional.empty(), parseOptionalYear(" ")); 335 assertEquals(Optional.empty(), parseOptionalYear("meow")); 336 337 assertEquals(Optional.empty(), parseOptionalYear("0")); 338 assertEquals(Optional.empty(), parseOptionalYear("00")); 339 assertEquals(Optional.empty(), parseOptionalYear("000")); 340 assertEquals(Optional.empty(), parseOptionalYear("0000")); 341 342 assertEquals(Optional.empty(), parseOptionalYear("1")); 343 assertEquals(Optional.empty(), parseOptionalYear("01")); 344 assertEquals(Optional.empty(), parseOptionalYear("001")); 345 assertEquals(Optional.empty(), parseOptionalYear("0001")); 346 347 // No sane way to determine year from two-digit date formats 348 assertEquals(Optional.empty(), parseOptionalYear("01-01-01")); 349 350 // Specific example from partner 351 assertEquals(Optional.empty(), parseOptionalYear("000 ")); 352 } 353 354 @Test testParseYear_Valid()355 public void testParseYear_Valid() throws Exception { 356 assertEquals(Optional.of(1900), parseOptionalYear("1900")); 357 assertEquals(Optional.of(2020), parseOptionalYear("2020")); 358 assertEquals(Optional.of(2020), parseOptionalYear(" 2020 ")); 359 assertEquals(Optional.of(2020), parseOptionalYear("01-01-2020")); 360 361 // Specific examples from partner 362 assertEquals(Optional.of(1984), parseOptionalYear("1984-06-26T07:00:00Z")); 363 assertEquals(Optional.of(2016), parseOptionalYear("Thu, 01 Sep 2016 10:11:12.123456 -0500")); 364 } 365 assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden, File dir)366 private static void assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden, 367 File dir) { 368 assertEquals(Pair.create(isScannable, isHidden), shouldScanPathAndIsPathHidden(dir)); 369 } 370 371 @Test testShouldScanPathAndIsPathHidden()372 public void testShouldScanPathAndIsPathHidden() { 373 for (String prefix : new String[] { 374 "/storage/emulated/0", 375 "/storage/emulated/0/Android/sandbox/com.example", 376 "/storage/0000-0000", 377 "/storage/0000-0000/Android/sandbox/com.example", 378 }) { 379 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix)); 380 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/meow")); 381 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/Android/meow")); 382 assertShouldScanPathAndIsPathHidden(true, false, 383 new File(prefix + "/Android/sandbox/meow")); 384 385 assertShouldScanPathAndIsPathHidden(true, true, new File(prefix + "/.meow/dir")); 386 387 assertShouldScanPathAndIsPathHidden(false, false, 388 new File(prefix + "/Android/data/meow")); 389 assertShouldScanPathAndIsPathHidden(false, false, 390 new File(prefix + "/Android/obb/meow")); 391 392 // When the path is not scannable, we don't care if it's hidden or not. 393 assertShouldScanPathAndIsPathHidden(false, false, 394 new File(prefix + "/Pictures/.thumbnails/meow")); 395 assertShouldScanPathAndIsPathHidden(false, false, 396 new File(prefix + "/Movies/.thumbnails/meow")); 397 assertShouldScanPathAndIsPathHidden(false, false, 398 new File(prefix + "/Music/.thumbnails/meow")); 399 } 400 } 401 assertShouldScanDirectory(File file)402 private static void assertShouldScanDirectory(File file) { 403 assertTrue(file.getAbsolutePath(), shouldScanDirectory(file)); 404 } 405 assertShouldntScanDirectory(File file)406 private static void assertShouldntScanDirectory(File file) { 407 assertFalse(file.getAbsolutePath(), shouldScanDirectory(file)); 408 } 409 410 @Test testShouldScanDirectory()411 public void testShouldScanDirectory() throws Exception { 412 for (String prefix : new String[] { 413 "/storage/emulated/0", 414 "/storage/emulated/0/Android/sandbox/com.example", 415 "/storage/0000-0000", 416 "/storage/0000-0000/Android/sandbox/com.example", 417 }) { 418 assertShouldScanDirectory(new File(prefix)); 419 assertShouldScanDirectory(new File(prefix + "/meow")); 420 assertShouldScanDirectory(new File(prefix + "/Android")); 421 assertShouldScanDirectory(new File(prefix + "/Android/meow")); 422 assertShouldScanDirectory(new File(prefix + "/Android/sandbox")); 423 assertShouldScanDirectory(new File(prefix + "/Android/sandbox/meow")); 424 assertShouldScanDirectory(new File(prefix + "/.meow")); 425 426 assertShouldntScanDirectory(new File(prefix + "/Android/data")); 427 assertShouldntScanDirectory(new File(prefix + "/Android/obb")); 428 429 assertShouldntScanDirectory(new File(prefix + "/Pictures/.thumbnails")); 430 assertShouldntScanDirectory(new File(prefix + "/Movies/.thumbnails")); 431 assertShouldntScanDirectory(new File(prefix + "/Music/.thumbnails")); 432 433 assertShouldScanDirectory(new File(prefix + "/DCIM/.thumbnails")); 434 } 435 } 436 assertDirectoryHidden(File file)437 private static void assertDirectoryHidden(File file) { 438 assertTrue(file.getAbsolutePath(), isDirectoryHidden(file)); 439 } 440 assertDirectoryNotHidden(File file)441 private static void assertDirectoryNotHidden(File file) { 442 assertFalse(file.getAbsolutePath(), isDirectoryHidden(file)); 443 } 444 445 @Test testIsDirectoryHidden()446 public void testIsDirectoryHidden() throws Exception { 447 for (String prefix : new String[] { 448 "/storage/emulated/0", 449 "/storage/emulated/0/Android/sandbox/com.example", 450 "/storage/0000-0000", 451 "/storage/0000-0000/Android/sandbox/com.example", 452 }) { 453 assertDirectoryNotHidden(new File(prefix)); 454 assertDirectoryNotHidden(new File(prefix + "/meow")); 455 456 assertDirectoryHidden(new File(prefix + "/.meow")); 457 } 458 459 460 final File nomediaFile = new File("storage/emulated/0/Download/meow", ".nomedia"); 461 try { 462 assertTrue(nomediaFile.getParentFile().mkdirs()); 463 assertTrue(nomediaFile.createNewFile()); 464 465 assertDirectoryHidden(nomediaFile.getParentFile()); 466 467 assertTrue(nomediaFile.delete()); 468 469 assertDirectoryNotHidden(nomediaFile.getParentFile()); 470 } finally { 471 nomediaFile.delete(); 472 nomediaFile.getParentFile().delete(); 473 } 474 } 475 476 @Test testIsFileHidden()477 public void testIsFileHidden() throws Exception { 478 assertFalse(isFileHidden( 479 new File("/storage/emulated/0/DCIM/IMG1024.JPG"))); 480 assertFalse(isFileHidden( 481 new File("/storage/emulated/0/DCIM/.pending-1577836800-IMG1024.JPG"))); 482 assertFalse(isFileHidden( 483 new File("/storage/emulated/0/DCIM/.trashed-1577836800-IMG1024.JPG"))); 484 assertTrue(isFileHidden( 485 new File("/storage/emulated/0/DCIM/.IMG1024.JPG"))); 486 } 487 488 @Test testIsZero()489 public void testIsZero() throws Exception { 490 assertFalse(ModernMediaScanner.isZero("")); 491 assertFalse(ModernMediaScanner.isZero("meow")); 492 assertFalse(ModernMediaScanner.isZero("1")); 493 assertFalse(ModernMediaScanner.isZero("01")); 494 assertFalse(ModernMediaScanner.isZero("010")); 495 496 assertTrue(ModernMediaScanner.isZero("0")); 497 assertTrue(ModernMediaScanner.isZero("00")); 498 assertTrue(ModernMediaScanner.isZero("000")); 499 } 500 501 @Test testPlaylistM3u()502 public void testPlaylistM3u() throws Exception { 503 doPlaylist(R.raw.test_m3u, "test.m3u"); 504 } 505 506 @Test testPlaylistPls()507 public void testPlaylistPls() throws Exception { 508 doPlaylist(R.raw.test_pls, "test.pls"); 509 } 510 511 @Test testPlaylistWpl()512 public void testPlaylistWpl() throws Exception { 513 doPlaylist(R.raw.test_wpl, "test.wpl"); 514 } 515 516 @Test testPlaylistXspf()517 public void testPlaylistXspf() throws Exception { 518 doPlaylist(R.raw.test_xspf, "test.xspf"); 519 } 520 doPlaylist(int res, String name)521 private void doPlaylist(int res, String name) throws Exception { 522 final File music = new File(mDir, "Music"); 523 music.mkdirs(); 524 stage(R.raw.test_audio, new File(music, "001.mp3")); 525 stage(R.raw.test_audio, new File(music, "002.mp3")); 526 stage(R.raw.test_audio, new File(music, "003.mp3")); 527 stage(R.raw.test_audio, new File(music, "004.mp3")); 528 stage(R.raw.test_audio, new File(music, "005.mp3")); 529 stage(res, new File(music, name)); 530 531 mModern.scanDirectory(mDir, REASON_UNKNOWN); 532 533 // We should see a new playlist with all three items as members 534 final long playlistId; 535 try (Cursor cursor = mIsolatedContext.getContentResolver().query( 536 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), 537 new String[] { FileColumns._ID }, 538 FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST, null, null)) { 539 assertTrue(cursor.moveToFirst()); 540 playlistId = cursor.getLong(0); 541 } 542 543 final Uri membersUri = MediaStore.Audio.Playlists.Members 544 .getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId); 545 try (Cursor cursor = mIsolatedResolver.query(membersUri, new String[] { 546 MediaColumns.DISPLAY_NAME 547 }, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER + " ASC")) { 548 assertEquals(5, cursor.getCount()); 549 cursor.moveToNext(); 550 assertEquals("001.mp3", cursor.getString(0)); 551 cursor.moveToNext(); 552 assertEquals("002.mp3", cursor.getString(0)); 553 cursor.moveToNext(); 554 assertEquals("003.mp3", cursor.getString(0)); 555 cursor.moveToNext(); 556 assertEquals("004.mp3", cursor.getString(0)); 557 cursor.moveToNext(); 558 assertEquals("005.mp3", cursor.getString(0)); 559 } 560 561 // Delete one of the media files and rescan 562 new File(music, "002.mp3").delete(); 563 new File(music, name).setLastModified(10L); 564 mModern.scanDirectory(mDir, REASON_UNKNOWN); 565 566 try (Cursor cursor = mIsolatedResolver.query(membersUri, new String[] { 567 MediaColumns.DISPLAY_NAME 568 }, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER + " ASC")) { 569 assertEquals(4, cursor.getCount()); 570 cursor.moveToNext(); 571 assertEquals("001.mp3", cursor.getString(0)); 572 cursor.moveToNext(); 573 assertEquals("003.mp3", cursor.getString(0)); 574 } 575 576 // Replace media file in a completely different location, which normally 577 // wouldn't match the exact playlist path, but we're willing to perform 578 // a relaxed search 579 final File soundtracks = new File(mDir, "Soundtracks"); 580 soundtracks.mkdirs(); 581 stage(R.raw.test_audio, new File(soundtracks, "002.mp3")); 582 stage(res, new File(music, name)); 583 584 mModern.scanDirectory(mDir, REASON_UNKNOWN); 585 586 try (Cursor cursor = mIsolatedResolver.query(membersUri, new String[] { 587 MediaColumns.DISPLAY_NAME 588 }, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER + " ASC")) { 589 assertEquals(5, cursor.getCount()); 590 cursor.moveToNext(); 591 assertEquals("001.mp3", cursor.getString(0)); 592 cursor.moveToNext(); 593 assertEquals("002.mp3", cursor.getString(0)); 594 cursor.moveToNext(); 595 assertEquals("003.mp3", cursor.getString(0)); 596 } 597 } 598 599 @Test testFilter()600 public void testFilter() throws Exception { 601 final File music = new File(mDir, "Music"); 602 music.mkdirs(); 603 stage(R.raw.test_audio, new File(music, "example.mp3")); 604 mModern.scanDirectory(mDir, REASON_UNKNOWN); 605 606 // Exact matches 607 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 608 .buildUpon().appendQueryParameter("filter", "artist").build()); 609 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 610 .buildUpon().appendQueryParameter("filter", "album").build()); 611 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 612 .buildUpon().appendQueryParameter("filter", "title").build()); 613 614 // Partial matches mid-string 615 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 616 .buildUpon().appendQueryParameter("filter", "ArT").build()); 617 618 // Filter should only apply to narrow collection type 619 assertQueryCount(0, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI 620 .buildUpon().appendQueryParameter("filter", "title").build()); 621 622 // Other unrelated search terms 623 assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 624 .buildUpon().appendQueryParameter("filter", "example").build()); 625 assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 626 .buildUpon().appendQueryParameter("filter", "チ").build()); 627 } 628 629 @Test testScan_Common()630 public void testScan_Common() throws Exception { 631 final File file = new File(mDir, "red.jpg"); 632 stage(R.raw.test_image, file); 633 634 mModern.scanDirectory(mDir, REASON_UNKNOWN); 635 636 // Confirm that we found new image and scanned it 637 final Uri uri; 638 try (Cursor cursor = mIsolatedResolver 639 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 640 assertEquals(1, cursor.getCount()); 641 cursor.moveToFirst(); 642 uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 643 cursor.getLong(cursor.getColumnIndex(MediaColumns._ID))); 644 assertEquals(1280, cursor.getLong(cursor.getColumnIndex(MediaColumns.WIDTH))); 645 assertEquals(720, cursor.getLong(cursor.getColumnIndex(MediaColumns.HEIGHT))); 646 } 647 648 // Write a totally different image and confirm that we automatically 649 // rescanned it 650 try (ParcelFileDescriptor pfd = mIsolatedResolver.openFile(uri, "wt", null)) { 651 final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); 652 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, 653 new FileOutputStream(pfd.getFileDescriptor())); 654 } 655 656 // Make sure out pending scan has finished 657 MediaStore.waitForIdle(mIsolatedResolver); 658 659 try (Cursor cursor = mIsolatedResolver 660 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 661 assertEquals(1, cursor.getCount()); 662 cursor.moveToFirst(); 663 assertEquals(32, cursor.getLong(cursor.getColumnIndex(MediaColumns.WIDTH))); 664 assertEquals(32, cursor.getLong(cursor.getColumnIndex(MediaColumns.HEIGHT))); 665 } 666 667 // Delete raw file and confirm it's cleaned up 668 file.delete(); 669 mModern.scanDirectory(mDir, REASON_UNKNOWN); 670 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 671 } 672 673 /** 674 * All file formats are thoroughly tested by {@code CtsProviderTestCases}, 675 * but to prove code coverage we also need to exercise manually here with a 676 * bare-bones scan operation. 677 */ 678 @Test testScan_Coverage()679 public void testScan_Coverage() throws Exception { 680 stage(R.raw.test_audio, new File(mDir, "audio.mp3")); 681 stage(R.raw.test_video, new File(mDir, "video.mp4")); 682 stage(R.raw.test_image, new File(mDir, "image.jpg")); 683 stage(R.raw.test_m3u, new File(mDir, "playlist.m3u")); 684 stage(R.raw.test_srt, new File(mDir, "subtitle.srt")); 685 stage(R.raw.test_txt, new File(mDir, "document.txt")); 686 stage(R.raw.test_bin, new File(mDir, "random.bin")); 687 688 mModern.scanDirectory(mDir, REASON_UNKNOWN); 689 } 690 691 @Test testScan_Nomedia_Dir()692 public void testScan_Nomedia_Dir() throws Exception { 693 final File red = new File(mDir, "red"); 694 final File blue = new File(mDir, "blue"); 695 red.mkdirs(); 696 blue.mkdirs(); 697 stage(R.raw.test_image, new File(red, "red.jpg")); 698 stage(R.raw.test_image, new File(blue, "blue.jpg")); 699 700 mModern.scanDirectory(mDir, REASON_UNKNOWN); 701 702 // We should have found both images 703 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 704 705 // Hide one directory, rescan, and confirm hidden 706 final File redNomedia = new File(red, ".nomedia"); 707 redNomedia.createNewFile(); 708 mModern.scanDirectory(mDir, REASON_UNKNOWN); 709 assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 710 711 // Unhide, rescan, and confirm visible again 712 redNomedia.delete(); 713 mModern.scanDirectory(mDir, REASON_UNKNOWN); 714 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 715 } 716 717 @Test testScan_Nomedia_File()718 public void testScan_Nomedia_File() throws Exception { 719 final File image = new File(mDir, "image.jpg"); 720 final File nomedia = new File(mDir, ".nomedia"); 721 stage(R.raw.test_image, image); 722 nomedia.createNewFile(); 723 724 // Direct scan with nomedia will change media type to MEDIA_TYPE_NONE 725 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 726 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 727 728 // Direct scan without nomedia means image 729 nomedia.delete(); 730 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 731 assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 732 733 // Direct scan again changes the media type to MEDIA_TYPE_NONE 734 nomedia.createNewFile(); 735 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 736 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 737 } 738 739 @Test testScanFileAndUpdateOwnerPackageName()740 public void testScanFileAndUpdateOwnerPackageName() throws Exception { 741 final File image = new File(mDir, "image.jpg"); 742 final String thisPackageName = InstrumentationRegistry.getContext().getPackageName(); 743 stage(R.raw.test_image, image); 744 745 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 746 // scanning the image file inserts new database entry with OWNER_PACKAGE_NAME as 747 // thisPackageName. 748 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN, thisPackageName)); 749 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 750 new String[] {MediaColumns.OWNER_PACKAGE_NAME}, null, null, null)) { 751 assertEquals(1, cursor.getCount()); 752 cursor.moveToNext(); 753 assertEquals(thisPackageName, cursor.getString(0)); 754 } 755 } 756 757 /** 758 * Verify fix for obscure bug which would cause us to delete files outside a 759 * directory that share a common prefix. 760 */ 761 @Test testScan_Prefix()762 public void testScan_Prefix() throws Exception { 763 final File dir = new File(mDir, "test"); 764 final File inside = new File(dir, "testfile.jpg"); 765 final File outside = new File(mDir, "testfile.jpg"); 766 767 dir.mkdirs(); 768 inside.createNewFile(); 769 outside.createNewFile(); 770 771 // Scanning from top means we get both items 772 mModern.scanDirectory(mDir, REASON_UNKNOWN); 773 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 774 775 // Scanning from middle means we still have both items 776 mModern.scanDirectory(dir, REASON_UNKNOWN); 777 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 778 } 779 assertQueryCount(int expected, Uri actualUri)780 private void assertQueryCount(int expected, Uri actualUri) { 781 try (Cursor cursor = mIsolatedResolver.query(actualUri, null, null, null, null)) { 782 assertEquals(expected, cursor.getCount()); 783 } 784 } 785 786 @Test testScan_audio_empty_title()787 public void testScan_audio_empty_title() throws Exception { 788 final File music = new File(mDir, "Music"); 789 final File audio = new File(music, "audio.mp3"); 790 791 music.mkdirs(); 792 stage(R.raw.test_audio_empty_title, audio); 793 794 mModern.scanFile(audio, REASON_UNKNOWN); 795 796 try (Cursor cursor = mIsolatedResolver 797 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 798 assertEquals(1, cursor.getCount()); 799 cursor.moveToFirst(); 800 assertEquals("audio", cursor.getString(cursor.getColumnIndex(MediaColumns.TITLE))); 801 } 802 } 803 804 /** 805 * Verify a narrow exception where we allow an {@code mp4} video file on 806 * disk to be indexed as an {@code m4a} audio file. 807 */ 808 @Test testScan_148316354()809 public void testScan_148316354() throws Exception { 810 final File file = new File(mDir, "148316354.mp4"); 811 stage(R.raw.test_m4a, file); 812 813 final Uri uri = mModern.scanFile(file, REASON_UNKNOWN); 814 try (Cursor cursor = mIsolatedResolver 815 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 816 assertEquals(1, cursor.getCount()); 817 cursor.moveToFirst(); 818 assertEquals("audio/mp4", 819 cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE))); 820 } 821 } 822 823 @Test testAlbumArtPattern()824 public void testAlbumArtPattern() throws Exception { 825 for (String path : new String[] { 826 "/storage/emulated/0/._abc", 827 "/storage/emulated/0/a._abc", 828 829 "/storage/emulated/0/AlbumArtSmall.jpg", 830 "/storage/emulated/0/albumartsmall.jpg", 831 832 "/storage/emulated/0/AlbumArt_{}_Small.jpg", 833 "/storage/emulated/0/albumart_{a}_small.jpg", 834 "/storage/emulated/0/AlbumArt_{}_Large.jpg", 835 "/storage/emulated/0/albumart_{a}_large.jpg", 836 837 "/storage/emulated/0/Folder.jpg", 838 "/storage/emulated/0/folder.jpg", 839 840 "/storage/emulated/0/AlbumArt.jpg", 841 "/storage/emulated/0/albumart.jpg", 842 "/storage/emulated/0/albumart1.jpg", 843 }) { 844 final File file = new File(path); 845 assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), isFileAlbumArt(file)); 846 } 847 848 for (String path : new String[] { 849 "/storage/emulated/0/AlbumArtLarge.jpg", 850 "/storage/emulated/0/albumartlarge.jpg", 851 }) { 852 final File file = new File(path); 853 assertTrue(isFileAlbumArt(file)); 854 } 855 } 856 } 857