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.MAX_EXCLUDE_DIRS; 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.scan.ModernMediaScanner.shouldScanPathAndIsPathHidden; 37 import static com.android.providers.media.util.FileUtils.isDirectoryHidden; 38 import static com.android.providers.media.util.FileUtils.isFileHidden; 39 40 import static com.google.common.truth.Truth.assertThat; 41 42 import static org.junit.Assert.assertEquals; 43 import static org.junit.Assert.assertFalse; 44 import static org.junit.Assert.assertNotNull; 45 import static org.junit.Assert.assertTrue; 46 import static org.mockito.ArgumentMatchers.eq; 47 import static org.mockito.Mockito.mock; 48 import static org.mockito.Mockito.when; 49 50 import android.Manifest; 51 import android.app.UiAutomation; 52 import android.content.ContentResolver; 53 import android.content.ContentUris; 54 import android.content.ContentValues; 55 import android.content.Context; 56 import android.database.Cursor; 57 import android.graphics.Bitmap; 58 import android.media.ExifInterface; 59 import android.media.MediaMetadataRetriever; 60 import android.net.Uri; 61 import android.os.Bundle; 62 import android.os.Environment; 63 import android.os.ParcelFileDescriptor; 64 import android.provider.MediaStore; 65 import android.provider.MediaStore.Audio.AudioColumns; 66 import android.provider.MediaStore.MediaColumns; 67 import android.text.format.DateUtils; 68 import android.util.Log; 69 import android.util.Pair; 70 71 import androidx.test.InstrumentationRegistry; 72 import androidx.test.filters.SdkSuppress; 73 import androidx.test.runner.AndroidJUnit4; 74 75 import com.android.providers.media.R; 76 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext; 77 import com.android.providers.media.tests.utils.Timer; 78 import com.android.providers.media.util.FileUtils; 79 80 import com.google.common.io.ByteStreams; 81 82 import org.junit.After; 83 import org.junit.Before; 84 import org.junit.Test; 85 import org.junit.runner.RunWith; 86 87 import java.io.File; 88 import java.io.FileInputStream; 89 import java.io.FileNotFoundException; 90 import java.io.FileOutputStream; 91 import java.io.IOException; 92 import java.io.InterruptedIOException; 93 import java.util.Locale; 94 import java.util.Optional; 95 96 @RunWith(AndroidJUnit4.class) 97 public class ModernMediaScannerTest { 98 // TODO: scan directory-vs-files and confirm identical results 99 100 private static final String TAG = "ModernMediaScannerTest"; 101 /** 102 * Number of times we should repeat an operation to get an average/max. 103 */ 104 private static final int COUNT_REPEAT = 5; 105 106 private File mDir; 107 108 private Context mIsolatedContext; 109 private ContentResolver mIsolatedResolver; 110 111 private ModernMediaScanner mModern; 112 113 @Before setUp()114 public void setUp() { 115 final Context context = InstrumentationRegistry.getTargetContext(); 116 InstrumentationRegistry.getInstrumentation().getUiAutomation() 117 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, 118 Manifest.permission.READ_COMPAT_CHANGE_CONFIG, 119 Manifest.permission.INTERACT_ACROSS_USERS); 120 121 mDir = new File(context.getExternalMediaDirs()[0], "test_" + System.nanoTime()); 122 mDir.mkdirs(); 123 FileUtils.deleteContents(mDir); 124 125 mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false); 126 mIsolatedResolver = mIsolatedContext.getContentResolver(); 127 128 mModern = new ModernMediaScanner(mIsolatedContext); 129 } 130 131 @After tearDown()132 public void tearDown() { 133 FileUtils.deleteContents(mDir); 134 InstrumentationRegistry.getInstrumentation() 135 .getUiAutomation().dropShellPermissionIdentity(); 136 } 137 138 @Test testSimple()139 public void testSimple() throws Exception { 140 assertNotNull(mModern.getContext()); 141 } 142 143 @Test testOverrideMimeType()144 public void testOverrideMimeType() throws Exception { 145 assertFalse(parseOptionalMimeType("image/png", null).isPresent()); 146 assertFalse(parseOptionalMimeType("image/png", "image").isPresent()); 147 assertFalse(parseOptionalMimeType("image/png", "im/im").isPresent()); 148 assertFalse(parseOptionalMimeType("image/png", "audio/x-shiny").isPresent()); 149 150 assertTrue(parseOptionalMimeType("image/png", "image/x-shiny").isPresent()); 151 assertEquals("image/x-shiny", 152 parseOptionalMimeType("image/png", "image/x-shiny").get()); 153 154 // Radical file type shifting isn't allowed 155 assertEquals(Optional.empty(), 156 parseOptionalMimeType("video/mp4", "audio/mpeg")); 157 } 158 159 @Test testParseOptional()160 public void testParseOptional() throws Exception { 161 assertFalse(parseOptional(null).isPresent()); 162 assertFalse(parseOptional("").isPresent()); 163 assertFalse(parseOptional(" ").isPresent()); 164 assertFalse(parseOptional("-1").isPresent()); 165 166 assertFalse(parseOptional(-1).isPresent()); 167 assertTrue(parseOptional(0).isPresent()); 168 assertTrue(parseOptional(1).isPresent()); 169 170 assertEquals("meow", parseOptional("meow").get()); 171 assertEquals(42, (int) parseOptional(42).get()); 172 } 173 174 @Test testParseOptionalOrZero()175 public void testParseOptionalOrZero() throws Exception { 176 assertFalse(parseOptionalOrZero(-1).isPresent()); 177 assertFalse(parseOptionalOrZero(0).isPresent()); 178 assertTrue(parseOptionalOrZero(1).isPresent()); 179 } 180 181 @Test testParseOptionalNumerator()182 public void testParseOptionalNumerator() throws Exception { 183 assertEquals(12, (int) parseOptionalNumerator("12").get()); 184 assertEquals(12, (int) parseOptionalNumerator("12/24").get()); 185 186 assertFalse(parseOptionalNumerator("/24").isPresent()); 187 } 188 189 @Test testParseOptionalOrientation()190 public void testParseOptionalOrientation() throws Exception { 191 assertEquals(0, 192 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_NORMAL).get()); 193 assertEquals(90, 194 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_90).get()); 195 assertEquals(180, 196 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_180).get()); 197 assertEquals(270, 198 (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_270).get()); 199 200 // We can't represent this as an orientation 201 assertFalse(parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).isPresent()); 202 } 203 204 @Test testParseOptionalImageResolution()205 public void testParseOptionalImageResolution() throws Exception { 206 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 207 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH))) 208 .thenReturn("640"); 209 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT))) 210 .thenReturn("480"); 211 assertEquals("640\u00d7480", parseOptionalImageResolution(mmr).get()); 212 } 213 214 @Test testParseOptionalVideoResolution()215 public void testParseOptionalVideoResolution() throws Exception { 216 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 217 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH))) 218 .thenReturn("640"); 219 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT))) 220 .thenReturn("480"); 221 assertEquals("640\u00d7480", parseOptionalVideoResolution(mmr).get()); 222 } 223 224 @Test testParseOptionalResolution()225 public void testParseOptionalResolution() throws Exception { 226 final ExifInterface exif = mock(ExifInterface.class); 227 when(exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).thenReturn("640"); 228 when(exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).thenReturn("480"); 229 assertEquals("640\u00d7480", parseOptionalResolution(exif).get()); 230 } 231 232 @Test testParseOptionalDate()233 public void testParseOptionalDate() throws Exception { 234 assertThat(parseOptionalDate("20200101T000000")).isEqualTo(Optional.of(1577836800000L)); 235 assertThat(parseOptionalDate("20200101T211205")).isEqualTo(Optional.of(1577913125000L)); 236 assertThat(parseOptionalDate("20200101T211205.000Z")) 237 .isEqualTo(Optional.of(1577913125000L)); 238 assertThat(parseOptionalDate("20200101T211205.123Z")) 239 .isEqualTo(Optional.of(1577913125123L)); 240 } 241 242 @Test testParseOptionalTrack()243 public void testParseOptionalTrack() throws Exception { 244 final MediaMetadataRetriever mmr = mock(MediaMetadataRetriever.class); 245 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER))) 246 .thenReturn("1/2"); 247 when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER))) 248 .thenReturn("4/12"); 249 assertEquals(1004, (int) parseOptionalTrack(mmr).get()); 250 } 251 252 @Test testParseDateTaken_Complete()253 public void testParseDateTaken_Complete() 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 // Offset is recorded, test both zeros 259 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-00:00"); 260 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 261 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00"); 262 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 263 264 // Offset is recorded, test both directions 265 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-07:00"); 266 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 267 exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+07:00"); 268 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 269 } 270 271 @Test testParseDateTaken_Gps()272 public void testParseDateTaken_Gps() throws Exception { 273 final File file = File.createTempFile("test", ".jpg"); 274 final ExifInterface exif = new ExifInterface(file); 275 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 276 277 // GPS tells us we're in UTC 278 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 279 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:14:00"); 280 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 281 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 282 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:20:00"); 283 assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get()); 284 285 // GPS tells us we're in -7 286 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 287 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:14:00"); 288 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 289 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 290 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:20:00"); 291 assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 292 293 // GPS tells us we're in +7 294 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 295 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:14:00"); 296 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 297 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28"); 298 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:20:00"); 299 assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get()); 300 301 // GPS beyond 24 hours isn't helpful 302 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:27"); 303 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34"); 304 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 305 exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:29"); 306 exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34"); 307 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 308 } 309 310 @Test testParseDateTaken_File()311 public void testParseDateTaken_File() throws Exception { 312 final File file = File.createTempFile("test", ".jpg"); 313 final ExifInterface exif = new ExifInterface(file); 314 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 315 316 // Modified tells us we're in UTC 317 assertEquals(1453972654000L, 318 (long) parseOptionalDateTaken(exif, 1453972654000L - 60000L).get()); 319 assertEquals(1453972654000L, 320 (long) parseOptionalDateTaken(exif, 1453972654000L + 60000L).get()); 321 322 // Modified tells us we're in -7 323 assertEquals(1453972654000L + 25200000L, 324 (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L - 60000L).get()); 325 assertEquals(1453972654000L + 25200000L, 326 (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L + 60000L).get()); 327 328 // Modified tells us we're in +7 329 assertEquals(1453972654000L - 25200000L, 330 (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L - 60000L).get()); 331 assertEquals(1453972654000L - 25200000L, 332 (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L + 60000L).get()); 333 334 // Modified beyond 24 hours isn't helpful 335 assertFalse(parseOptionalDateTaken(exif, 1453972654000L - 86400000L).isPresent()); 336 assertFalse(parseOptionalDateTaken(exif, 1453972654000L + 86400000L).isPresent()); 337 } 338 339 @Test testParseDateTaken_Hopeless()340 public void testParseDateTaken_Hopeless() throws Exception { 341 final File file = File.createTempFile("test", ".jpg"); 342 final ExifInterface exif = new ExifInterface(file); 343 exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34"); 344 345 // Offset is completely missing, and no useful GPS or modified time 346 assertFalse(parseOptionalDateTaken(exif, 0L).isPresent()); 347 } 348 349 @Test testParseYear_Invalid()350 public void testParseYear_Invalid() throws Exception { 351 assertEquals(Optional.empty(), parseOptionalYear(null)); 352 assertEquals(Optional.empty(), parseOptionalYear("")); 353 assertEquals(Optional.empty(), parseOptionalYear(" ")); 354 assertEquals(Optional.empty(), parseOptionalYear("meow")); 355 356 assertEquals(Optional.empty(), parseOptionalYear("0")); 357 assertEquals(Optional.empty(), parseOptionalYear("00")); 358 assertEquals(Optional.empty(), parseOptionalYear("000")); 359 assertEquals(Optional.empty(), parseOptionalYear("0000")); 360 361 assertEquals(Optional.empty(), parseOptionalYear("1")); 362 assertEquals(Optional.empty(), parseOptionalYear("01")); 363 assertEquals(Optional.empty(), parseOptionalYear("001")); 364 assertEquals(Optional.empty(), parseOptionalYear("0001")); 365 366 // No sane way to determine year from two-digit date formats 367 assertEquals(Optional.empty(), parseOptionalYear("01-01-01")); 368 369 // Specific example from partner 370 assertEquals(Optional.empty(), parseOptionalYear("000 ")); 371 } 372 373 @Test testParseYear_Valid()374 public void testParseYear_Valid() throws Exception { 375 assertEquals(Optional.of(1900), parseOptionalYear("1900")); 376 assertEquals(Optional.of(2020), parseOptionalYear("2020")); 377 assertEquals(Optional.of(2020), parseOptionalYear(" 2020 ")); 378 assertEquals(Optional.of(2020), parseOptionalYear("01-01-2020")); 379 380 // Specific examples from partner 381 assertEquals(Optional.of(1984), parseOptionalYear("1984-06-26T07:00:00Z")); 382 assertEquals(Optional.of(2016), parseOptionalYear("Thu, 01 Sep 2016 10:11:12.123456 -0500")); 383 } 384 assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden, File dir)385 private static void assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden, 386 File dir) { 387 assertEquals(Pair.create(isScannable, isHidden), shouldScanPathAndIsPathHidden(dir)); 388 } 389 390 @Test testShouldScanPathAndIsPathHidden()391 public void testShouldScanPathAndIsPathHidden() { 392 for (String prefix : new String[] { 393 "/storage/emulated/0", 394 "/storage/0000-0000", 395 }) { 396 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix)); 397 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/meow")); 398 assertShouldScanPathAndIsPathHidden(true, false, new File(prefix + "/Android/meow")); 399 400 assertShouldScanPathAndIsPathHidden(true, true, new File(prefix + "/.meow/dir")); 401 402 assertShouldScanPathAndIsPathHidden(false, false, 403 new File(prefix + "/Android/data/meow")); 404 assertShouldScanPathAndIsPathHidden(false, false, 405 new File(prefix + "/Android/obb/meow")); 406 407 // When the path is not scannable, we don't care if it's hidden or not. 408 assertShouldScanPathAndIsPathHidden(false, false, 409 new File(prefix + "/Pictures/.thumbnails/meow")); 410 assertShouldScanPathAndIsPathHidden(false, false, 411 new File(prefix + "/Movies/.thumbnails/meow")); 412 assertShouldScanPathAndIsPathHidden(false, false, 413 new File(prefix + "/Music/.thumbnails/meow")); 414 415 assertShouldScanPathAndIsPathHidden(false, false, 416 new File(prefix + "/.transforms/transcode")); 417 } 418 } 419 assertVisibleFolder(File dir)420 private void assertVisibleFolder(File dir) throws Exception { 421 final File nomediaFile = new File(dir, ".nomedia"); 422 423 if (!nomediaFile.getParentFile().exists()) { 424 assertTrue(nomediaFile.getParentFile().mkdirs()); 425 } 426 try { 427 if (!nomediaFile.exists()) { 428 executeShellCommand("touch " + nomediaFile.getAbsolutePath()); 429 assertTrue(nomediaFile.exists()); 430 } 431 assertShouldScanPathAndIsPathHidden(true, false, dir); 432 } finally { 433 executeShellCommand("rm " + nomediaFile.getAbsolutePath()); 434 } 435 } 436 437 /** 438 * b/168830497: Test that default folders and Camera folder are always visible 439 */ 440 @Test testVisibleDefaultFolders()441 public void testVisibleDefaultFolders() throws Exception { 442 final File root = new File("storage/emulated/0"); 443 444 // Top level directories should always be visible 445 for (String dirName : FileUtils.DEFAULT_FOLDER_NAMES) { 446 final File defaultFolder = new File(root, dirName); 447 assertVisibleFolder(defaultFolder); 448 } 449 450 // DCIM/Camera should always be visible 451 final File cameraDir = new File(root, Environment.DIRECTORY_DCIM + "/" + "Camera"); 452 assertVisibleFolder(cameraDir); 453 } 454 assertShouldScanDirectory(File file)455 private static void assertShouldScanDirectory(File file) { 456 assertTrue(file.getAbsolutePath(), shouldScanDirectory(file)); 457 } 458 assertShouldntScanDirectory(File file)459 private static void assertShouldntScanDirectory(File file) { 460 assertFalse(file.getAbsolutePath(), shouldScanDirectory(file)); 461 } 462 463 @Test testShouldScanDirectory()464 public void testShouldScanDirectory() throws Exception { 465 for (String prefix : new String[] { 466 "/storage/emulated/0", 467 "/storage/0000-0000", 468 }) { 469 assertShouldScanDirectory(new File(prefix)); 470 assertShouldScanDirectory(new File(prefix + "/meow")); 471 assertShouldScanDirectory(new File(prefix + "/Android")); 472 assertShouldScanDirectory(new File(prefix + "/Android/meow")); 473 assertShouldScanDirectory(new File(prefix + "/.meow")); 474 475 assertShouldntScanDirectory(new File(prefix + "/Android/data")); 476 assertShouldntScanDirectory(new File(prefix + "/Android/obb")); 477 assertShouldntScanDirectory(new File(prefix + "/Android/sandbox")); 478 479 assertShouldntScanDirectory(new File(prefix + "/Pictures/.thumbnails")); 480 assertShouldntScanDirectory(new File(prefix + "/Movies/.thumbnails")); 481 assertShouldntScanDirectory(new File(prefix + "/Music/.thumbnails")); 482 483 assertShouldScanDirectory(new File(prefix + "/DCIM/.thumbnails")); 484 assertShouldntScanDirectory(new File(prefix + "/.transforms")); 485 } 486 } 487 assertDirectoryHidden(File file)488 private static void assertDirectoryHidden(File file) { 489 assertTrue(file.getAbsolutePath(), isDirectoryHidden(file)); 490 } 491 assertDirectoryNotHidden(File file)492 private static void assertDirectoryNotHidden(File file) { 493 assertFalse(file.getAbsolutePath(), isDirectoryHidden(file)); 494 } 495 496 @Test testIsDirectoryHidden()497 public void testIsDirectoryHidden() throws Exception { 498 for (String prefix : new String[] { 499 "/storage/emulated/0", 500 "/storage/0000-0000", 501 }) { 502 assertDirectoryNotHidden(new File(prefix)); 503 assertDirectoryNotHidden(new File(prefix + "/meow")); 504 505 assertDirectoryHidden(new File(prefix + "/.meow")); 506 } 507 508 509 final File nomediaFile = new File("storage/emulated/0/Download/meow", ".nomedia"); 510 try { 511 assertTrue(nomediaFile.getParentFile().mkdirs()); 512 assertTrue(nomediaFile.createNewFile()); 513 514 assertDirectoryHidden(nomediaFile.getParentFile()); 515 516 assertTrue(nomediaFile.delete()); 517 518 assertDirectoryNotHidden(nomediaFile.getParentFile()); 519 } finally { 520 nomediaFile.delete(); 521 nomediaFile.getParentFile().delete(); 522 } 523 } 524 525 @Test testIsFileHidden()526 public void testIsFileHidden() throws Exception { 527 assertFalse(isFileHidden( 528 new File("/storage/emulated/0/DCIM/IMG1024.JPG"))); 529 assertFalse(isFileHidden( 530 new File("/storage/emulated/0/DCIM/.pending-1577836800-IMG1024.JPG"))); 531 assertFalse(isFileHidden( 532 new File("/storage/emulated/0/DCIM/.trashed-1577836800-IMG1024.JPG"))); 533 assertTrue(isFileHidden( 534 new File("/storage/emulated/0/DCIM/.IMG1024.JPG"))); 535 } 536 537 @Test testIsZero()538 public void testIsZero() throws Exception { 539 assertFalse(ModernMediaScanner.isZero("")); 540 assertFalse(ModernMediaScanner.isZero("meow")); 541 assertFalse(ModernMediaScanner.isZero("1")); 542 assertFalse(ModernMediaScanner.isZero("01")); 543 assertFalse(ModernMediaScanner.isZero("010")); 544 545 assertTrue(ModernMediaScanner.isZero("0")); 546 assertTrue(ModernMediaScanner.isZero("00")); 547 assertTrue(ModernMediaScanner.isZero("000")); 548 } 549 550 @Test testFilter()551 public void testFilter() throws Exception { 552 final File music = new File(mDir, "Music"); 553 music.mkdirs(); 554 stage(R.raw.test_audio, new File(music, "example.mp3")); 555 mModern.scanDirectory(mDir, REASON_UNKNOWN); 556 557 // Exact matches 558 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 559 .buildUpon().appendQueryParameter("filter", "artist").build()); 560 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 561 .buildUpon().appendQueryParameter("filter", "album").build()); 562 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 563 .buildUpon().appendQueryParameter("filter", "title").build()); 564 565 // Partial matches mid-string 566 assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 567 .buildUpon().appendQueryParameter("filter", "ArT").build()); 568 569 // Filter should only apply to narrow collection type 570 assertQueryCount(0, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI 571 .buildUpon().appendQueryParameter("filter", "title").build()); 572 573 // Other unrelated search terms 574 assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 575 .buildUpon().appendQueryParameter("filter", "example").build()); 576 assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 577 .buildUpon().appendQueryParameter("filter", "チ").build()); 578 } 579 580 @Test testScan_Common()581 public void testScan_Common() throws Exception { 582 final File file = new File(mDir, "red.jpg"); 583 stage(R.raw.test_image, file); 584 585 mModern.scanDirectory(mDir, REASON_UNKNOWN); 586 587 // Confirm that we found new image and scanned it 588 final Uri uri; 589 try (Cursor cursor = mIsolatedResolver 590 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 591 assertEquals(1, cursor.getCount()); 592 cursor.moveToFirst(); 593 uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 594 cursor.getLong(cursor.getColumnIndex(MediaColumns._ID))); 595 assertEquals(1280, cursor.getLong(cursor.getColumnIndex(MediaColumns.WIDTH))); 596 assertEquals(720, cursor.getLong(cursor.getColumnIndex(MediaColumns.HEIGHT))); 597 } 598 599 // Write a totally different image and confirm that we automatically 600 // rescanned it 601 try (ParcelFileDescriptor pfd = mIsolatedResolver.openFile(uri, "wt", null)) { 602 final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); 603 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, 604 new FileOutputStream(pfd.getFileDescriptor())); 605 } 606 607 // Make sure out pending scan has finished 608 MediaStore.waitForIdle(mIsolatedResolver); 609 610 try (Cursor cursor = mIsolatedResolver 611 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 612 assertEquals(1, cursor.getCount()); 613 cursor.moveToFirst(); 614 assertEquals(32, cursor.getLong(cursor.getColumnIndex(MediaColumns.WIDTH))); 615 assertEquals(32, cursor.getLong(cursor.getColumnIndex(MediaColumns.HEIGHT))); 616 } 617 618 // Delete raw file and confirm it's cleaned up 619 file.delete(); 620 mModern.scanDirectory(mDir, REASON_UNKNOWN); 621 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 622 } 623 624 /** 625 * All file formats are thoroughly tested by {@code CtsProviderTestCases}, 626 * but to prove code coverage we also need to exercise manually here with a 627 * bare-bones scan operation. 628 */ 629 @Test testScan_Coverage()630 public void testScan_Coverage() throws Exception { 631 stage(R.raw.test_audio, new File(mDir, "audio.mp3")); 632 stage(R.raw.test_video, new File(mDir, "video.mp4")); 633 stage(R.raw.test_image, new File(mDir, "image.jpg")); 634 stage(R.raw.test_m3u, new File(mDir, "playlist.m3u")); 635 stage(R.raw.test_srt, new File(mDir, "subtitle.srt")); 636 stage(R.raw.test_txt, new File(mDir, "document.txt")); 637 stage(R.raw.test_bin, new File(mDir, "random.bin")); 638 639 mModern.scanDirectory(mDir, REASON_UNKNOWN); 640 } 641 642 @Test testScan_missingDir()643 public void testScan_missingDir() throws Exception { 644 File newDir = new File(mDir, "new-dir"); 645 // Below shouldn't crash 646 mModern.scanDirectory(newDir, REASON_UNKNOWN); 647 648 newDir = new File(Environment.getStorageDirectory(), "new-dir"); 649 // Below shouldn't crash 650 mModern.scanDirectory(newDir, REASON_UNKNOWN); 651 } 652 653 @Test testScan_Nomedia_Dir()654 public void testScan_Nomedia_Dir() throws Exception { 655 final File redDir = new File(mDir, "red"); 656 final File blueDir = new File(mDir, "blue"); 657 redDir.mkdirs(); 658 blueDir.mkdirs(); 659 stage(R.raw.test_image, new File(redDir, "red.jpg")); 660 stage(R.raw.test_image, new File(blueDir, "blue.jpg")); 661 662 mModern.scanDirectory(mDir, REASON_UNKNOWN); 663 664 // We should have found both images 665 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 666 667 // Hide one directory, rescan, and confirm hidden 668 final File redNomedia = new File(redDir, ".nomedia"); 669 redNomedia.createNewFile(); 670 mModern.scanDirectory(mDir, REASON_UNKNOWN); 671 assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 672 673 // Unhide, rescan, and confirm visible again 674 redNomedia.delete(); 675 mModern.scanDirectory(mDir, REASON_UNKNOWN); 676 assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 677 } 678 679 @Test testScan_MaxExcludeNomediaDirs_DoesNotThrowException()680 public void testScan_MaxExcludeNomediaDirs_DoesNotThrowException() throws Exception { 681 // Create MAX_EXCLUDE_DIRS + 50 nomedia dirs in mDir 682 // (Need to add 50 as MAX_EXCLUDE_DIRS is a safe limit; 683 // 499 would have been too close to the exception limit) 684 // Mark them as non-dirty so that they are excluded from scans 685 for (int i = 0 ; i < (MAX_EXCLUDE_DIRS + 50) ; i++) { 686 createCleanNomediaDir(mDir); 687 } 688 689 final File redDir = new File(mDir, "red"); 690 redDir.mkdirs(); 691 stage(R.raw.test_image, new File(redDir, "red.jpg")); 692 693 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 694 mModern.scanDirectory(mDir, REASON_UNKNOWN); 695 assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 696 } 697 createCleanNomediaDir(File dir)698 private void createCleanNomediaDir(File dir) throws Exception { 699 final File nomediaDir = new File(dir, "test_" + System.nanoTime()); 700 nomediaDir.mkdirs(); 701 final File nomedia = new File(nomediaDir, ".nomedia"); 702 nomedia.createNewFile(); 703 704 FileUtils.setDirectoryDirty(nomediaDir, false); 705 assertThat(FileUtils.isDirectoryDirty(nomediaDir)).isFalse(); 706 } 707 708 @Test testScan_Nomedia_File()709 public void testScan_Nomedia_File() throws Exception { 710 final File image = new File(mDir, "image.jpg"); 711 final File nomedia = new File(mDir, ".nomedia"); 712 stage(R.raw.test_image, image); 713 nomedia.createNewFile(); 714 715 // Direct scan with nomedia will change media type to MEDIA_TYPE_NONE 716 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 717 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 718 719 // Direct scan without nomedia means image 720 nomedia.delete(); 721 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 722 assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 723 724 // Direct scan again changes the media type to MEDIA_TYPE_NONE 725 nomedia.createNewFile(); 726 assertNotNull(mModern.scanFile(image, REASON_UNKNOWN)); 727 assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 728 } 729 730 @Test testScan_missingFile()731 public void testScan_missingFile() throws Exception { 732 File image = new File(mDir, "image.jpg"); 733 assertThat(mModern.scanFile(image, REASON_UNKNOWN)).isNull(); 734 735 image = new File(Environment.getStorageDirectory(), "image.jpg"); 736 assertThat(mModern.scanFile(image, REASON_UNKNOWN)).isNull(); 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 @Test 805 @SdkSuppress(minSdkVersion = 31, codeName = "S") testScan_audio_recording()806 public void testScan_audio_recording() throws Exception { 807 final File music = new File(mDir, "Recordings"); 808 final File audio = new File(music, "audio.mp3"); 809 810 music.mkdirs(); 811 stage(R.raw.test_audio, audio); 812 813 mModern.scanFile(audio, REASON_UNKNOWN); 814 815 try (Cursor cursor = mIsolatedResolver 816 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 817 assertEquals(1, cursor.getCount()); 818 cursor.moveToFirst(); 819 assertEquals(1, cursor.getInt(cursor.getColumnIndex(AudioColumns.IS_RECORDING))); 820 assertEquals(0, cursor.getInt(cursor.getColumnIndex(AudioColumns.IS_MUSIC))); 821 } 822 } 823 824 /** 825 * Verify a narrow exception where we allow an {@code mp4} video file on 826 * disk to be indexed as an {@code m4a} audio file. 827 */ 828 @Test testScan_148316354()829 public void testScan_148316354() throws Exception { 830 final File file = new File(mDir, "148316354.mp4"); 831 stage(R.raw.test_m4a, file); 832 833 final Uri uri = mModern.scanFile(file, REASON_UNKNOWN); 834 try (Cursor cursor = mIsolatedResolver 835 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 836 assertEquals(1, cursor.getCount()); 837 cursor.moveToFirst(); 838 assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE))) 839 .isEqualTo("audio/mp4"); 840 assertThat(cursor.getString(cursor.getColumnIndex(AudioColumns.IS_MUSIC))) 841 .isEqualTo("1"); 842 843 } 844 } 845 846 @Test testScan_audioMp4_notRescanIfUnchanged()847 public void testScan_audioMp4_notRescanIfUnchanged() throws Exception { 848 final File file = new File(mDir, "176522651.m4a"); 849 stage(R.raw.test_m4a, file); 850 851 // We trigger a scan twice, but we expect the second scan to be skipped since there were 852 // no changes. 853 mModern.scanFile(file, REASON_UNKNOWN); 854 mModern.scanFile(file, REASON_UNKNOWN); 855 856 try (Cursor cursor = 857 mIsolatedResolver.query( 858 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 859 null, null, null, null)) { 860 assertThat(cursor.getCount()).isEqualTo(1); 861 cursor.moveToFirst(); 862 String added = cursor.getString(cursor.getColumnIndex(MediaColumns.GENERATION_ADDED)); 863 String modified = 864 cursor.getString(cursor.getColumnIndex(MediaColumns.GENERATION_MODIFIED)); 865 assertThat(modified).isEqualTo(added); 866 } 867 } 868 869 /** 870 * If there is a scan action between invoking {@link ContentResolver#insert} and 871 * {@link ContentResolver#openFileDescriptor}, it should not raise 872 * (@link FileNotFoundException}. 873 */ 874 @Test testScan_166063754()875 public void testScan_166063754() throws Exception { 876 Uri collection = MediaStore.Images.Media 877 .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); 878 879 ContentValues values = new ContentValues(); 880 values.put(MediaStore.Images.Media.DISPLAY_NAME, mDir.getName() + "_166063754.jpg"); 881 values.put(MediaStore.Images.Media.IS_PENDING, 1); 882 883 Uri uri = mIsolatedResolver.insert(collection, values); 884 885 File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); 886 mModern.scanFile(dir, REASON_UNKNOWN); 887 try { 888 mIsolatedResolver.openFileDescriptor(uri, "w", null); 889 } catch (FileNotFoundException e) { 890 throw new AssertionError("Can't open uri " + uri, e); 891 } 892 } 893 894 @Test testAlbumArtPattern()895 public void testAlbumArtPattern() throws Exception { 896 for (String path : new String[] { 897 "/storage/emulated/0/._abc", 898 "/storage/emulated/0/a._abc", 899 900 "/storage/emulated/0/AlbumArtSmall.jpg", 901 "/storage/emulated/0/albumartsmall.jpg", 902 903 "/storage/emulated/0/AlbumArt_{}_Small.jpg", 904 "/storage/emulated/0/albumart_{a}_small.jpg", 905 "/storage/emulated/0/AlbumArt_{}_Large.jpg", 906 "/storage/emulated/0/albumart_{a}_large.jpg", 907 908 "/storage/emulated/0/Folder.jpg", 909 "/storage/emulated/0/folder.jpg", 910 911 "/storage/emulated/0/AlbumArt.jpg", 912 "/storage/emulated/0/albumart.jpg", 913 "/storage/emulated/0/albumart1.jpg", 914 }) { 915 final File file = new File(path); 916 assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), isFileAlbumArt(file)); 917 } 918 919 for (String path : new String[] { 920 "/storage/emulated/0/AlbumArtLarge.jpg", 921 "/storage/emulated/0/albumartlarge.jpg", 922 }) { 923 final File file = new File(path); 924 assertTrue(isFileAlbumArt(file)); 925 } 926 } 927 928 @Test testScan_BitmapFile()929 public void testScan_BitmapFile() throws Exception { 930 final File bmp = new File(mDir, "image.bmp"); 931 stage(R.raw.test_bmp, bmp); 932 933 final Uri uri = mModern.scanFile(bmp, REASON_UNKNOWN); 934 try (Cursor cursor = mIsolatedResolver 935 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 936 assertEquals(1, cursor.getCount()); 937 cursor.moveToFirst(); 938 assertEquals(1280, 939 cursor.getInt(cursor.getColumnIndex(MediaColumns.WIDTH))); 940 assertEquals(720, 941 cursor.getInt(cursor.getColumnIndex(MediaColumns.HEIGHT))); 942 } 943 } 944 945 @Test testScan_deleteStaleRowWithExpiredPendingFile()946 public void testScan_deleteStaleRowWithExpiredPendingFile() throws Exception { 947 final String displayName = "audio.mp3"; 948 final long dateExpires = (System.currentTimeMillis() - 5 * DateUtils.DAY_IN_MILLIS) / 1000; 949 final String expiredName = String.format( 950 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName); 951 final File audio = new File(mDir, expiredName); 952 stage(R.raw.test_audio, audio); 953 954 // files should exist 955 assertThat(audio.exists()).isTrue(); 956 957 // scan file, row is added 958 mModern.scanFile(audio, REASON_UNKNOWN); 959 final Bundle queryArgs = new Bundle(); 960 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 961 962 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 963 null, queryArgs, null)) { 964 assertThat(cursor.getCount()).isEqualTo(1); 965 } 966 967 // Delete the pending file to make the row is stale 968 executeShellCommand("rm " + audio.getAbsolutePath()); 969 assertThat(audio.exists()).isFalse(); 970 971 // the row still exists 972 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 973 null, queryArgs, null)) { 974 assertThat(cursor.getCount()).isEqualTo(1); 975 } 976 977 mModern.scanFile(audio, REASON_UNKNOWN); 978 979 // ScanFile above deleted stale expired pending row, hence we shouldn't see 980 // the pending row in query result 981 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 982 null, queryArgs, null)) { 983 assertThat(cursor.getCount()).isEqualTo(0); 984 } 985 } 986 987 @Test testScan_keepStaleRowWithNonExpiredPendingFile()988 public void testScan_keepStaleRowWithNonExpiredPendingFile() throws Exception { 989 final String displayName = "audio.mp3"; 990 final long dateExpires = (System.currentTimeMillis() + 2 * DateUtils.DAY_IN_MILLIS) / 1000; 991 final String expiredName = String.format( 992 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName); 993 final File audio = new File(mDir, expiredName); 994 stage(R.raw.test_audio, audio); 995 996 // file should exist 997 assertThat(audio.exists()).isTrue(); 998 999 // scan file, row is added 1000 mModern.scanFile(audio, REASON_UNKNOWN); 1001 final Bundle queryArgs = new Bundle(); 1002 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 1003 1004 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1005 null, queryArgs, null)) { 1006 assertThat(cursor.getCount()).isEqualTo(1); 1007 } 1008 1009 // Delete the pending file to make the row is stale 1010 executeShellCommand("rm " + audio.getAbsolutePath()); 1011 assertThat(audio.exists()).isFalse(); 1012 1013 // the row still exists 1014 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1015 null, queryArgs, null)) { 1016 assertThat(cursor.getCount()).isEqualTo(1); 1017 } 1018 1019 mModern.scanFile(audio, REASON_UNKNOWN); 1020 1021 // ScanFile above didn't delete stale pending row which is not expired, hence 1022 // we still see the pending row in query result 1023 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1024 null, queryArgs, null)) { 1025 assertThat(cursor.getCount()).isEqualTo(1); 1026 } 1027 } 1028 1029 @Test testScan_deleteStaleRowWithExpiredTrashedFile()1030 public void testScan_deleteStaleRowWithExpiredTrashedFile() throws Exception { 1031 final String displayName = "audio.mp3"; 1032 final long dateExpires = (System.currentTimeMillis() - 5 * DateUtils.DAY_IN_MILLIS) / 1000; 1033 final String expiredName = String.format( 1034 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName); 1035 final File audio = new File(mDir, expiredName); 1036 stage(R.raw.test_audio, audio); 1037 1038 // file should exist 1039 assertThat(audio.exists()).isTrue(); 1040 1041 // scan file, row is added 1042 mModern.scanFile(audio, REASON_UNKNOWN); 1043 final Bundle queryArgs = new Bundle(); 1044 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 1045 1046 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1047 null, queryArgs, null)) { 1048 assertThat(cursor.getCount()).isEqualTo(1); 1049 } 1050 1051 // Delete the trashed file to make the row is stale 1052 executeShellCommand("rm " + audio.getAbsolutePath()); 1053 assertThat(audio.exists()).isFalse(); 1054 1055 // the row still exists 1056 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1057 null, queryArgs, null)) { 1058 assertThat(cursor.getCount()).isEqualTo(1); 1059 } 1060 1061 mModern.scanFile(audio, REASON_UNKNOWN); 1062 1063 // ScanFile above deleted stale expired trashed row, hence we shouldn't see 1064 // the trashed row in query result 1065 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1066 null, queryArgs, null)) { 1067 assertThat(cursor.getCount()).isEqualTo(0); 1068 } 1069 } 1070 1071 @Test testScan_deleteStaleRowWithNonExpiredTrashedFile()1072 public void testScan_deleteStaleRowWithNonExpiredTrashedFile() throws Exception { 1073 final String displayName = "audio.mp3"; 1074 final long dateExpires = (System.currentTimeMillis() + 2 * DateUtils.DAY_IN_MILLIS) / 1000; 1075 final String expiredName = String.format( 1076 Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName); 1077 final File audio = new File(mDir, expiredName); 1078 stage(R.raw.test_audio, audio); 1079 1080 // file should exist 1081 assertThat(audio.exists()).isTrue(); 1082 1083 // scan file, row is added 1084 mModern.scanFile(audio, REASON_UNKNOWN); 1085 final Bundle queryArgs = new Bundle(); 1086 queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 1087 1088 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1089 null, queryArgs, null)) { 1090 assertThat(cursor.getCount()).isEqualTo(1); 1091 } 1092 1093 // Delete the trashed file to make the row is stale 1094 executeShellCommand("rm " + audio.getAbsolutePath()); 1095 assertThat(audio.exists()).isFalse(); 1096 1097 // the row still exists 1098 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1099 null, queryArgs, null)) { 1100 assertThat(cursor.getCount()).isEqualTo(1); 1101 } 1102 1103 mModern.scanFile(audio, REASON_UNKNOWN); 1104 1105 // ScanFile above deleted stale trashed row that is not expired, hence we 1106 // shouldn't see the trashed row in query result 1107 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1108 null, queryArgs, null)) { 1109 assertThat(cursor.getCount()).isEqualTo(0); 1110 } 1111 } 1112 1113 @Test testScan_deleteStaleRow()1114 public void testScan_deleteStaleRow() throws Exception { 1115 final String displayName = "audio.mp3"; 1116 final File audio = new File(mDir, displayName); 1117 stage(R.raw.test_audio, audio); 1118 1119 // file should exist 1120 assertThat(audio.exists()).isTrue(); 1121 1122 // scan file, row is added 1123 mModern.scanFile(audio, REASON_UNKNOWN); 1124 1125 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1126 null, null, null)) { 1127 assertThat(cursor.getCount()).isEqualTo(1); 1128 } 1129 1130 // Delete the file to make the row is stale 1131 executeShellCommand("rm " + audio.getAbsolutePath()); 1132 assertThat(audio.exists()).isFalse(); 1133 1134 // the row still exists 1135 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1136 null, null, null)) { 1137 assertThat(cursor.getCount()).isEqualTo(1); 1138 } 1139 1140 mModern.scanFile(audio, REASON_UNKNOWN); 1141 1142 // ScanFile above deleted stale row, hence we shouldn't see the row in query result 1143 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1144 null, null, null)) { 1145 assertThat(cursor.getCount()).isEqualTo(0); 1146 } 1147 } 1148 1149 /** 1150 * Executes a shell command. 1151 */ executeShellCommand(String command)1152 public static String executeShellCommand(String command) throws IOException { 1153 int attempt = 0; 1154 while (attempt++ < 5) { 1155 try { 1156 return executeShellCommandInternal(command); 1157 } catch (InterruptedIOException e) { 1158 // Hmm, we had trouble executing the shell command; the best we 1159 // can do is try again a few more times 1160 Log.v(TAG, "Trouble executing " + command + "; trying again", e); 1161 } 1162 } 1163 throw new IOException("Failed to execute " + command); 1164 } 1165 executeShellCommandInternal(String cmd)1166 private static String executeShellCommandInternal(String cmd) throws IOException { 1167 UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 1168 try (FileInputStream output = new FileInputStream( 1169 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) { 1170 return new String(ByteStreams.toByteArray(output)); 1171 } 1172 } 1173 1174 @Test testScan_largeXmpData()1175 public void testScan_largeXmpData() throws Exception { 1176 final File image = new File(mDir, "large_xmp.mp4"); 1177 stage(R.raw.large_xmp, image); 1178 assertTrue(image.exists()); 1179 1180 mModern.scanDirectory(mDir, REASON_UNKNOWN); 1181 1182 try (Cursor cursor = mIsolatedResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 1183 new String[] { MediaColumns.XMP }, null, null, null)) { 1184 assertEquals(1, cursor.getCount()); 1185 cursor.moveToFirst(); 1186 assertEquals(0, cursor.getBlob(0).length); 1187 } 1188 } 1189 1190 @Test testNoOpScan_NoMediaDirs()1191 public void testNoOpScan_NoMediaDirs() throws Exception { 1192 File nomedia = new File(mDir, ".nomedia"); 1193 assertThat(nomedia.createNewFile()).isTrue(); 1194 for (int i = 0; i < 100; i++) { 1195 File file = new File(mDir, "file_" + System.nanoTime()); 1196 assertThat(file.createNewFile()).isTrue(); 1197 } 1198 Timer firstDirScan = new Timer("firstDirScan"); 1199 firstDirScan.start(); 1200 // Time taken : preVisitDirectory + 100 visitFiles 1201 mModern.scanDirectory(mDir, REASON_UNKNOWN); 1202 firstDirScan.stop(); 1203 firstDirScan.dumpResults(); 1204 1205 // Time taken : preVisitDirectory 1206 Timer noOpDirScan = new Timer("noOpDirScan1"); 1207 for (int i = 0; i < COUNT_REPEAT; i++) { 1208 noOpDirScan.start(); 1209 mModern.scanDirectory(mDir, REASON_UNKNOWN); 1210 noOpDirScan.stop(); 1211 } 1212 noOpDirScan.dumpResults(); 1213 assertThat(noOpDirScan.getMaxDurationMillis()).isLessThan( 1214 firstDirScan.getMaxDurationMillis()); 1215 1216 // Creating new file in the nomedia dir by a non-M_E_S app should not set nomedia dir dirty. 1217 File file = new File(mDir, "file_" + System.nanoTime()); 1218 assertThat(file.createNewFile()).isTrue(); 1219 1220 // The dir should not be dirty and subsequest scans should not scan the entire directory. 1221 // Time taken : preVisitDirectory 1222 noOpDirScan = new Timer("noOpDirScan2"); 1223 for (int i = 0; i < COUNT_REPEAT; i++) { 1224 noOpDirScan.start(); 1225 mModern.scanDirectory(mDir, REASON_UNKNOWN); 1226 noOpDirScan.stop(); 1227 } 1228 noOpDirScan.dumpResults(); 1229 assertThat(noOpDirScan.getMaxDurationMillis()).isLessThan( 1230 firstDirScan.getMaxDurationMillis()); 1231 } 1232 1233 @Test testScan_TrackNumber()1234 public void testScan_TrackNumber() throws Exception { 1235 final File music = new File(mDir, "Music"); 1236 final File audio = new File(music, "audio.mp3"); 1237 1238 music.mkdirs(); 1239 stage(R.raw.test_audio, audio); 1240 1241 mModern.scanFile(audio, REASON_UNKNOWN); 1242 1243 try (Cursor cursor = mIsolatedResolver 1244 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 1245 assertEquals(1, cursor.getCount()); 1246 cursor.moveToFirst(); 1247 assertEquals(2, cursor.getInt(cursor.getColumnIndex(AudioColumns.TRACK))); 1248 } 1249 1250 stage(R.raw.test_audio_empty_track_number, audio); 1251 1252 mModern.scanFile(audio, REASON_UNKNOWN); 1253 1254 try (Cursor cursor = mIsolatedResolver 1255 .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) { 1256 assertEquals(1, cursor.getCount()); 1257 cursor.moveToFirst(); 1258 assertThat(cursor.getString(cursor.getColumnIndex(AudioColumns.TRACK))).isNull(); 1259 } 1260 } 1261 } 1262