1 /* 2 * Copyright (C) 2008 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.util; 18 19 import static android.os.ParcelFileDescriptor.MODE_APPEND; 20 import static android.os.ParcelFileDescriptor.MODE_CREATE; 21 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 22 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; 23 import static android.os.ParcelFileDescriptor.MODE_TRUNCATE; 24 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY; 25 import static android.system.OsConstants.F_OK; 26 import static android.system.OsConstants.O_APPEND; 27 import static android.system.OsConstants.O_CREAT; 28 import static android.system.OsConstants.O_RDONLY; 29 import static android.system.OsConstants.O_RDWR; 30 import static android.system.OsConstants.O_TRUNC; 31 import static android.system.OsConstants.O_WRONLY; 32 import static android.system.OsConstants.R_OK; 33 import static android.system.OsConstants.W_OK; 34 import static android.system.OsConstants.X_OK; 35 import static android.text.format.DateUtils.DAY_IN_MILLIS; 36 import static android.text.format.DateUtils.HOUR_IN_MILLIS; 37 import static android.text.format.DateUtils.WEEK_IN_MILLIS; 38 39 import static com.android.providers.media.util.FileUtils.buildUniqueFile; 40 import static com.android.providers.media.util.FileUtils.extractDisplayName; 41 import static com.android.providers.media.util.FileUtils.extractFileExtension; 42 import static com.android.providers.media.util.FileUtils.extractFileName; 43 import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath; 44 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName; 45 import static com.android.providers.media.util.FileUtils.extractRelativePath; 46 import static com.android.providers.media.util.FileUtils.extractTopLevelDir; 47 import static com.android.providers.media.util.FileUtils.extractVolumeName; 48 import static com.android.providers.media.util.FileUtils.extractVolumePath; 49 import static com.android.providers.media.util.FileUtils.fromFuseFile; 50 import static com.android.providers.media.util.FileUtils.isDataOrObbPath; 51 import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath; 52 import static com.android.providers.media.util.FileUtils.isDirectoryHidden; 53 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory; 54 import static com.android.providers.media.util.FileUtils.isFileHidden; 55 import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath; 56 import static com.android.providers.media.util.FileUtils.toFuseFile; 57 import static com.android.providers.media.util.FileUtils.translateModeAccessToPosix; 58 import static com.android.providers.media.util.FileUtils.translateModePfdToPosix; 59 import static com.android.providers.media.util.FileUtils.translateModePosixToPfd; 60 import static com.android.providers.media.util.FileUtils.translateModePosixToString; 61 import static com.android.providers.media.util.FileUtils.translateModeStringToPosix; 62 63 import static com.google.common.truth.Truth.assertThat; 64 import static com.google.common.truth.Truth.assertWithMessage; 65 66 import static org.junit.Assert.assertEquals; 67 import static org.junit.Assert.assertFalse; 68 import static org.junit.Assert.assertNull; 69 import static org.junit.Assert.assertThrows; 70 import static org.junit.Assert.assertTrue; 71 import static org.junit.Assert.fail; 72 73 import android.content.ContentValues; 74 import android.os.Environment; 75 import android.os.SystemProperties; 76 import android.provider.MediaStore; 77 import android.provider.MediaStore.Audio.AudioColumns; 78 import android.provider.MediaStore.MediaColumns; 79 import android.text.TextUtils; 80 81 import androidx.test.InstrumentationRegistry; 82 import androidx.test.runner.AndroidJUnit4; 83 84 import com.google.common.collect.Range; 85 86 import org.junit.After; 87 import org.junit.Assume; 88 import org.junit.Before; 89 import org.junit.Test; 90 import org.junit.runner.RunWith; 91 92 import java.io.File; 93 import java.io.FileNotFoundException; 94 import java.io.IOException; 95 import java.io.RandomAccessFile; 96 import java.util.Arrays; 97 import java.util.Collections; 98 import java.util.HashSet; 99 import java.util.List; 100 import java.util.Locale; 101 import java.util.Optional; 102 103 @RunWith(AndroidJUnit4.class) 104 public class FileUtilsTest { 105 // Exposing here since it is also used by MediaProviderTest.java 106 public static final int MAX_FILENAME_BYTES = FileUtils.MAX_FILENAME_BYTES; 107 108 /** 109 * To help avoid flaky tests, give ourselves a unique nonce to be used for 110 * all filesystem paths, so that we don't risk conflicting with previous 111 * test runs. 112 */ 113 private static final String NONCE = String.valueOf(System.nanoTime()); 114 115 private static final String TEST_DIRECTORY_NAME = "FileUtilsTestDirectory" + NONCE; 116 private static final String TEST_FILE_NAME = "FileUtilsTestFile" + NONCE; 117 118 private File mTarget; 119 private File mDcimTarget; 120 private File mDeleteTarget; 121 private File mDownloadTarget; 122 private File mTestDownloadDir; 123 124 @Before setUp()125 public void setUp() throws Exception { 126 mTarget = InstrumentationRegistry.getTargetContext().getCacheDir(); 127 FileUtils.deleteContents(mTarget); 128 129 mDcimTarget = new File(mTarget, "DCIM"); 130 mDcimTarget.mkdirs(); 131 132 mDeleteTarget = mDcimTarget; 133 134 mDownloadTarget = new File(Environment.getExternalStorageDirectory(), 135 Environment.DIRECTORY_DOWNLOADS); 136 mTestDownloadDir = new File(mDownloadTarget, TEST_DIRECTORY_NAME); 137 mTestDownloadDir.mkdirs(); 138 } 139 140 @After tearDown()141 public void tearDown() throws Exception { 142 FileUtils.deleteContents(mTarget); 143 FileUtils.deleteContents(mTestDownloadDir); 144 } 145 touch(String name, long age)146 private void touch(String name, long age) throws Exception { 147 final File file = new File(mDeleteTarget, name); 148 file.createNewFile(); 149 file.setLastModified(System.currentTimeMillis() - age); 150 } 151 152 @Test testString()153 public void testString() throws Exception { 154 final File file = new File(mTarget, String.valueOf(System.nanoTime())); 155 156 // Verify initial empty state 157 assertFalse(FileUtils.readString(file).isPresent()); 158 159 // Verify simple writing and reading 160 FileUtils.writeString(file, Optional.of("meow")); 161 assertTrue(FileUtils.readString(file).isPresent()); 162 assertEquals("meow", FileUtils.readString(file).get()); 163 164 // Verify empty writing deletes file 165 FileUtils.writeString(file, Optional.empty()); 166 assertFalse(FileUtils.readString(file).isPresent()); 167 168 // Verify reading from a file with more than 4096 chars 169 try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 170 raf.setLength(4097); 171 } 172 assertEquals(Optional.empty(), FileUtils.readString(file)); 173 174 // Verify reading from non existing file. 175 file.delete(); 176 assertEquals(Optional.empty(), FileUtils.readString(file)); 177 178 } 179 180 @Test testDeleteOlderEmptyDir()181 public void testDeleteOlderEmptyDir() throws Exception { 182 FileUtils.deleteOlderFiles(mDeleteTarget, 10, WEEK_IN_MILLIS); 183 assertDirContents(); 184 } 185 186 @Test testDeleteOlderTypical()187 public void testDeleteOlderTypical() throws Exception { 188 touch("file1", HOUR_IN_MILLIS); 189 touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 190 touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 191 touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 192 touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 193 assertTrue(FileUtils.deleteOlderFiles(mDeleteTarget, 3, DAY_IN_MILLIS)); 194 assertDirContents("file1", "file2", "file3"); 195 } 196 197 @Test testDeleteOlderInFuture()198 public void testDeleteOlderInFuture() throws Exception { 199 touch("file1", -HOUR_IN_MILLIS); 200 touch("file2", HOUR_IN_MILLIS); 201 touch("file3", WEEK_IN_MILLIS); 202 assertTrue(FileUtils.deleteOlderFiles(mDeleteTarget, 0, DAY_IN_MILLIS)); 203 assertDirContents("file1", "file2"); 204 205 touch("file1", -HOUR_IN_MILLIS); 206 touch("file2", HOUR_IN_MILLIS); 207 touch("file3", WEEK_IN_MILLIS); 208 assertTrue(FileUtils.deleteOlderFiles(mDeleteTarget, 0, DAY_IN_MILLIS)); 209 assertDirContents("file1", "file2"); 210 } 211 212 @Test testDeleteOlderOnlyAge()213 public void testDeleteOlderOnlyAge() throws Exception { 214 touch("file1", HOUR_IN_MILLIS); 215 touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 216 touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 217 touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 218 touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 219 assertTrue(FileUtils.deleteOlderFiles(mDeleteTarget, 0, DAY_IN_MILLIS)); 220 assertFalse(FileUtils.deleteOlderFiles(mDeleteTarget, 0, DAY_IN_MILLIS)); 221 assertDirContents("file1"); 222 } 223 224 @Test testDeleteOlderOnlyCount()225 public void testDeleteOlderOnlyCount() throws Exception { 226 touch("file1", HOUR_IN_MILLIS); 227 touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 228 touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 229 touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 230 touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); 231 assertTrue(FileUtils.deleteOlderFiles(mDeleteTarget, 2, 0)); 232 assertFalse(FileUtils.deleteOlderFiles(mDeleteTarget, 2, 0)); 233 assertDirContents("file1", "file2"); 234 } 235 236 @Test testTranslateMode()237 public void testTranslateMode() throws Exception { 238 assertTranslate("r", O_RDONLY, MODE_READ_ONLY); 239 240 assertTranslate("rw", O_RDWR | O_CREAT, 241 MODE_READ_WRITE | MODE_CREATE); 242 assertTranslate("rwt", O_RDWR | O_CREAT | O_TRUNC, 243 MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); 244 assertTranslate("rwa", O_RDWR | O_CREAT | O_APPEND, 245 MODE_READ_WRITE | MODE_CREATE | MODE_APPEND); 246 247 assertTranslate("w", O_WRONLY | O_CREAT, 248 MODE_WRITE_ONLY | MODE_CREATE | MODE_CREATE); 249 assertTranslate("wt", O_WRONLY | O_CREAT | O_TRUNC, 250 MODE_WRITE_ONLY | MODE_CREATE | MODE_TRUNCATE); 251 assertTranslate("wa", O_WRONLY | O_CREAT | O_APPEND, 252 MODE_WRITE_ONLY | MODE_CREATE | MODE_APPEND); 253 } 254 255 @Test testMalformedTransate_int()256 public void testMalformedTransate_int() throws Exception { 257 try { 258 // The non-standard Linux access mode 3 should throw 259 // an IllegalArgumentException. 260 translateModePosixToPfd(O_RDWR | O_WRONLY); 261 fail(); 262 } catch (IllegalArgumentException expected) { 263 } 264 } 265 266 @Test testMalformedTransate_string()267 public void testMalformedTransate_string() throws Exception { 268 try { 269 // The non-standard Linux access mode 3 should throw 270 // an IllegalArgumentException. 271 translateModePosixToString(O_RDWR | O_WRONLY); 272 fail(); 273 } catch (IllegalArgumentException expected) { 274 } 275 } 276 277 @Test testTranslateMode_Invalid()278 public void testTranslateMode_Invalid() throws Exception { 279 try { 280 translateModeStringToPosix("rwx"); 281 fail(); 282 } catch (IllegalArgumentException expected) { 283 } 284 try { 285 translateModeStringToPosix(""); 286 fail(); 287 } catch (IllegalArgumentException expected) { 288 } 289 } 290 291 @Test testTranslateMode_Access()292 public void testTranslateMode_Access() throws Exception { 293 assertEquals(O_RDONLY, translateModeAccessToPosix(F_OK)); 294 assertEquals(O_RDONLY, translateModeAccessToPosix(R_OK)); 295 assertEquals(O_WRONLY, translateModeAccessToPosix(W_OK)); 296 assertEquals(O_RDWR, translateModeAccessToPosix(R_OK | W_OK)); 297 assertEquals(O_RDWR, translateModeAccessToPosix(R_OK | W_OK | X_OK)); 298 } 299 assertTranslate(String string, int posix, int pfd)300 private static void assertTranslate(String string, int posix, int pfd) { 301 assertEquals(posix, translateModeStringToPosix(string)); 302 assertEquals(string, translateModePosixToString(posix)); 303 assertEquals(pfd, translateModePosixToPfd(posix)); 304 assertEquals(posix, translateModePfdToPosix(pfd)); 305 } 306 307 @Test testContains()308 public void testContains() throws Exception { 309 assertTrue(FileUtils.contains(new File("/"), new File("/moo.txt"))); 310 assertTrue(FileUtils.contains(new File("/"), new File("/"))); 311 312 assertTrue(FileUtils.contains(new File("/sdcard"), new File("/sdcard"))); 313 assertTrue(FileUtils.contains(new File("/sdcard/"), new File("/sdcard/"))); 314 315 assertTrue(FileUtils.contains(new File("/sdcard"), new File("/sdcard/moo.txt"))); 316 assertTrue(FileUtils.contains(new File("/sdcard/"), new File("/sdcard/moo.txt"))); 317 318 assertFalse(FileUtils.contains(new File("/sdcard"), new File("/moo.txt"))); 319 assertFalse(FileUtils.contains(new File("/sdcard/"), new File("/moo.txt"))); 320 321 assertFalse(FileUtils.contains(new File("/sdcard"), new File("/sdcard.txt"))); 322 assertFalse(FileUtils.contains(new File("/sdcard/"), new File("/sdcard.txt"))); 323 } 324 325 @Test testValidFatFilename()326 public void testValidFatFilename() throws Exception { 327 assertTrue(FileUtils.isValidFatFilename("a")); 328 assertTrue(FileUtils.isValidFatFilename("foo bar.baz")); 329 assertTrue(FileUtils.isValidFatFilename("foo.bar.baz")); 330 assertTrue(FileUtils.isValidFatFilename(".bar")); 331 assertTrue(FileUtils.isValidFatFilename("foo.bar")); 332 assertTrue(FileUtils.isValidFatFilename("foo bar")); 333 assertTrue(FileUtils.isValidFatFilename("foo+bar")); 334 assertTrue(FileUtils.isValidFatFilename("foo,bar")); 335 336 assertFalse(FileUtils.isValidFatFilename("foo*bar")); 337 assertFalse(FileUtils.isValidFatFilename("foo?bar")); 338 assertFalse(FileUtils.isValidFatFilename("foo<bar")); 339 assertFalse(FileUtils.isValidFatFilename(null)); 340 assertFalse(FileUtils.isValidFatFilename(".")); 341 assertFalse(FileUtils.isValidFatFilename("../foo")); 342 assertFalse(FileUtils.isValidFatFilename("/foo")); 343 344 assertEquals(".._foo", FileUtils.buildValidFatFilename("../foo")); 345 assertEquals("_foo", FileUtils.buildValidFatFilename("/foo")); 346 assertEquals(".foo", FileUtils.buildValidFatFilename(".foo")); 347 assertEquals("foo.bar", FileUtils.buildValidFatFilename("foo.bar")); 348 assertEquals("foo_bar__baz", FileUtils.buildValidFatFilename("foo?bar**baz")); 349 } 350 351 @Test testTrimFilename()352 public void testTrimFilename() throws Exception { 353 assertEquals("short.txt", FileUtils.trimFilename("short.txt", 16)); 354 assertEquals("extrem...eme.txt", FileUtils.trimFilename("extremelylongfilename.txt", 16)); 355 356 final String unicode = "a\u03C0\u03C0\u03C0\u03C0z"; 357 assertEquals("a\u03C0\u03C0\u03C0\u03C0z", FileUtils.trimFilename(unicode, 10)); 358 assertEquals("a\u03C0...\u03C0z", FileUtils.trimFilename(unicode, 9)); 359 assertEquals("a...\u03C0z", FileUtils.trimFilename(unicode, 8)); 360 assertEquals("a...\u03C0z", FileUtils.trimFilename(unicode, 7)); 361 assertEquals("a...z", FileUtils.trimFilename(unicode, 6)); 362 } 363 364 @Test testBuildUniqueFile_normal()365 public void testBuildUniqueFile_normal() throws Exception { 366 assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test")); 367 assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg")); 368 assertNameEquals("test.jpeg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpeg")); 369 assertNameEquals("TEst.JPeg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", "TEst.JPeg")); 370 assertNameEquals(".test.jpg", FileUtils.buildUniqueFile(mTarget, "image/jpeg", ".test")); 371 assertNameEquals("test.png.jpg", 372 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.png.jpg")); 373 assertNameEquals("test.png.jpg", 374 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.png")); 375 376 assertNameEquals("test.flac", FileUtils.buildUniqueFile(mTarget, "audio/flac", "test")); 377 assertNameEquals("test.flac", FileUtils.buildUniqueFile(mTarget, "audio/flac", "test.flac")); 378 assertNameEquals("test.flac", 379 FileUtils.buildUniqueFile(mTarget, "application/x-flac", "test")); 380 assertNameEquals("test.flac", 381 FileUtils.buildUniqueFile(mTarget, "application/x-flac", "test.flac")); 382 } 383 384 @Test testBuildUniqueFile_unknown()385 public void testBuildUniqueFile_unknown() throws Exception { 386 assertNameEquals("test", 387 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", "test")); 388 assertNameEquals("test.jpg", 389 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", "test.jpg")); 390 assertNameEquals(".test", 391 FileUtils.buildUniqueFile(mTarget, "application/octet-stream", ".test")); 392 393 assertNameEquals("test", FileUtils.buildUniqueFile(mTarget, "lolz/lolz", "test")); 394 assertNameEquals("test.lolz", FileUtils.buildUniqueFile(mTarget, "lolz/lolz", "test.lolz")); 395 } 396 397 @Test testBuildUniqueFile_increment()398 public void testBuildUniqueFile_increment() throws Exception { 399 assertNameEquals("test.jpg", 400 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg")); 401 new File(mTarget, "test.jpg").createNewFile(); 402 assertNameEquals("test (1).jpg", 403 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg")); 404 new File(mTarget, "test (1).jpg").createNewFile(); 405 assertNameEquals("test (2).jpg", 406 FileUtils.buildUniqueFile(mTarget, "image/jpeg", "test.jpg")); 407 } 408 409 @Test testBuildUniqueFile_increment_hidden()410 public void testBuildUniqueFile_increment_hidden() throws Exception { 411 assertNameEquals(".hidden.jpg", 412 FileUtils.buildUniqueFile(mTarget, "image/jpeg", ".hidden.jpg")); 413 new File(mTarget, ".hidden.jpg").createNewFile(); 414 assertNameEquals(".hidden (1).jpg", 415 FileUtils.buildUniqueFile(mTarget, "image/jpeg", ".hidden.jpg")); 416 } 417 418 @Test testBuildUniqueFile_mimeless()419 public void testBuildUniqueFile_mimeless() throws Exception { 420 assertNameEquals("test.jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg")); 421 new File(mTarget, "test.jpg").createNewFile(); 422 assertNameEquals("test (1).jpg", FileUtils.buildUniqueFile(mTarget, "test.jpg")); 423 424 assertNameEquals("test", FileUtils.buildUniqueFile(mTarget, "test")); 425 new File(mTarget, "test").createNewFile(); 426 assertNameEquals("test (1)", FileUtils.buildUniqueFile(mTarget, "test")); 427 428 assertNameEquals("test.foo.bar", FileUtils.buildUniqueFile(mTarget, "test.foo.bar")); 429 new File(mTarget, "test.foo.bar").createNewFile(); 430 assertNameEquals("test.foo (1).bar", FileUtils.buildUniqueFile(mTarget, "test.foo.bar")); 431 } 432 433 /** 434 * Verify that we generate unique filenames that meet the JEITA DCF 435 * specification when writing into directories like {@code DCIM}. 436 */ 437 @Test testBuildUniqueFile_DCF_strict()438 public void testBuildUniqueFile_DCF_strict() throws Exception { 439 assertNameEquals("IMG_0100.JPG", 440 buildUniqueFile(mDcimTarget, "IMG_0100.JPG")); 441 442 touch(mDcimTarget, "IMG_0999.JPG"); 443 assertNameEquals("IMG_0998.JPG", 444 buildUniqueFile(mDcimTarget, "IMG_0998.JPG")); 445 assertNameEquals("IMG_1000.JPG", 446 buildUniqueFile(mDcimTarget, "IMG_0999.JPG")); 447 assertNameEquals("IMG_1000.JPG", 448 buildUniqueFile(mDcimTarget, "IMG_1000.JPG")); 449 450 touch(mDcimTarget, "IMG_1000.JPG"); 451 assertNameEquals("IMG_1001.JPG", 452 buildUniqueFile(mDcimTarget, "IMG_0999.JPG")); 453 454 // We can't step beyond standard numbering 455 touch(mDcimTarget, "IMG_9999.JPG"); 456 try { 457 buildUniqueFile(mDcimTarget, "IMG_9999.JPG"); 458 fail(); 459 } catch (FileNotFoundException expected) { 460 } 461 } 462 463 /** 464 * Verify that we generate unique filenames that meet the JEITA DCF 465 * specification when writing into directories like {@code DCIM}. 466 * 467 * See b/174120008 for context. 468 */ 469 @Test testBuildUniqueFile_DCF_strict_differentLocale()470 public void testBuildUniqueFile_DCF_strict_differentLocale() throws Exception { 471 Locale defaultLocale = Locale.getDefault(); 472 try { 473 Locale.setDefault(new Locale("ar", "SA")); 474 testBuildUniqueFile_DCF_strict(); 475 } 476 finally { 477 Locale.setDefault(defaultLocale); 478 } 479 } 480 481 /** 482 * Verify that we generate unique filenames that look valid compared to other 483 * {@code DCIM} filenames. These technically aren't part of the official 484 * JEITA DCF specification. 485 */ 486 @Test testBuildUniqueFile_DCF_relaxed()487 public void testBuildUniqueFile_DCF_relaxed() throws Exception { 488 touch(mDcimTarget, "IMG_20190102_030405.jpg"); 489 assertNameEquals("IMG_20190102_030405~2.jpg", 490 buildUniqueFile(mDcimTarget, "IMG_20190102_030405.jpg")); 491 492 touch(mDcimTarget, "IMG_20190102_030405~2.jpg"); 493 assertNameEquals("IMG_20190102_030405~3.jpg", 494 buildUniqueFile(mDcimTarget, "IMG_20190102_030405.jpg")); 495 assertNameEquals("IMG_20190102_030405~3.jpg", 496 buildUniqueFile(mDcimTarget, "IMG_20190102_030405~2.jpg")); 497 } 498 499 /** 500 * Verify that we generate unique filenames that look valid compared to other 501 * {@code DCIM} filenames. These technically aren't part of the official 502 * JEITA DCF specification. 503 * 504 * See b/174120008 for context. 505 */ 506 @Test testBuildUniqueFile_DCF_relaxed_differentLocale()507 public void testBuildUniqueFile_DCF_relaxed_differentLocale() throws Exception { 508 Locale defaultLocale = Locale.getDefault(); 509 try { 510 Locale.setDefault(new Locale("ar", "SA")); 511 testBuildUniqueFile_DCF_relaxed(); 512 } finally { 513 Locale.setDefault(defaultLocale); 514 } 515 } 516 517 @Test testGetAbsoluteExtendedPath()518 public void testGetAbsoluteExtendedPath() throws Exception { 519 assertEquals("/storage/emulated/0/DCIM/.trashed-1888888888-test.jpg", 520 FileUtils.getAbsoluteExtendedPath( 521 "/storage/emulated/0/DCIM/.trashed-1621147340-test.jpg", 1888888888)); 522 } 523 524 @Test testExtractVolumePath()525 public void testExtractVolumePath() throws Exception { 526 assertEquals("/storage/emulated/0/", 527 extractVolumePath("/storage/emulated/0/foo.jpg")); 528 assertEquals("/storage/0000-0000/", 529 extractVolumePath("/storage/0000-0000/foo.jpg")); 530 } 531 532 @Test testExtractVolumeName()533 public void testExtractVolumeName() throws Exception { 534 assertEquals(MediaStore.VOLUME_EXTERNAL_PRIMARY, 535 extractVolumeName("/storage/emulated/0/foo.jpg")); 536 assertEquals("0000-0000", 537 extractVolumeName("/storage/0000-0000/foo.jpg")); 538 } 539 540 @Test testExtractRelativePath()541 public void testExtractRelativePath() throws Exception { 542 for (String prefix : new String[] { 543 "/storage/emulated/0/", 544 "/storage/0000-0000/" 545 }) { 546 assertEquals("/", 547 extractRelativePath(prefix + "foo.jpg")); 548 assertEquals("DCIM/", 549 extractRelativePath(prefix + "DCIM/foo.jpg")); 550 assertEquals("DCIM/My Vacation/", 551 extractRelativePath(prefix + "DCIM/My Vacation/foo.jpg")); 552 assertEquals("Pictures/", 553 extractRelativePath(prefix + "DCIM/../Pictures/.//foo.jpg")); 554 assertEquals("/", 555 extractRelativePath(prefix + "DCIM/Pictures/./..//..////foo.jpg")); 556 assertEquals("Android/data/", 557 extractRelativePath(prefix + "DCIM/foo.jpg/.//../../Android/data/poc")); 558 } 559 560 assertEquals(null, extractRelativePath("/sdcard/\\\u0000")); 561 } 562 563 @Test testExtractTopLevelDir()564 public void testExtractTopLevelDir() throws Exception { 565 for (String prefix : new String[] { 566 "/storage/emulated/0/", 567 "/storage/0000-0000/" 568 }) { 569 assertEquals(null, 570 extractTopLevelDir(prefix + "foo.jpg")); 571 assertEquals("DCIM", 572 extractTopLevelDir(prefix + "DCIM/foo.jpg")); 573 assertEquals("DCIM", 574 extractTopLevelDir(prefix + "DCIM/My Vacation/foo.jpg")); 575 } 576 } 577 578 @Test testExtractTopLevelDirWithRelativePathSegments()579 public void testExtractTopLevelDirWithRelativePathSegments() throws Exception { 580 assertEquals(null, 581 extractTopLevelDir(new String[] { null })); 582 assertEquals("DCIM", 583 extractTopLevelDir(new String[] { "DCIM" })); 584 assertEquals("DCIM", 585 extractTopLevelDir(new String[] { "DCIM", "My Vacation" })); 586 587 assertEquals(null, 588 extractTopLevelDir(new String[] { "AppClone" }, "AppClone")); 589 assertEquals("DCIM", 590 extractTopLevelDir(new String[] { "AppClone", "DCIM" }, "AppClone")); 591 assertEquals("DCIM", 592 extractTopLevelDir(new String[] { "AppClone", "DCIM", "My Vacation" }, "AppClone")); 593 594 assertEquals("Test", 595 extractTopLevelDir(new String[] { "Test" }, "AppClone")); 596 assertEquals("Test", 597 extractTopLevelDir(new String[] { "Test", "DCIM" }, "AppClone")); 598 assertEquals("Test", 599 extractTopLevelDir(new String[] { "Test", "DCIM", "My Vacation" }, "AppClone")); 600 } 601 602 @Test testExtractTopLevelDirForCrossUser()603 public void testExtractTopLevelDirForCrossUser() throws Exception { 604 Assume.assumeTrue(FileUtils.isCrossUserEnabled()); 605 606 final String crossUserRoot = SystemProperties.get("external_storage.cross_user.root", null); 607 Assume.assumeFalse(TextUtils.isEmpty(crossUserRoot)); 608 609 for (String prefix : new String[] { 610 "/storage/emulated/0/", 611 "/storage/0000-0000/" 612 }) { 613 assertEquals(null, 614 extractTopLevelDir(prefix + "foo.jpg")); 615 assertEquals("DCIM", 616 extractTopLevelDir(prefix + "DCIM/foo.jpg")); 617 assertEquals("DCIM", 618 extractTopLevelDir(prefix + "DCIM/My Vacation/foo.jpg")); 619 620 assertEquals(null, 621 extractTopLevelDir(prefix + crossUserRoot + "/foo.jpg")); 622 assertEquals("DCIM", 623 extractTopLevelDir(prefix + crossUserRoot + "/DCIM/foo.jpg")); 624 assertEquals("DCIM", 625 extractTopLevelDir(prefix + crossUserRoot + "/DCIM/My Vacation/foo.jpg")); 626 627 assertEquals("Test", 628 extractTopLevelDir(prefix + "Test/DCIM/foo.jpg")); 629 assertEquals("Test", 630 extractTopLevelDir(prefix + "Test/DCIM/My Vacation/foo.jpg")); 631 } 632 } 633 634 @Test testExtractDisplayName()635 public void testExtractDisplayName() throws Exception { 636 for (String probe : new String[] { 637 "foo.bar.baz", 638 "/foo.bar.baz", 639 "/foo.bar.baz/", 640 "/sdcard/foo.bar.baz", 641 "/sdcard/foo.bar.baz/", 642 }) { 643 assertEquals(probe, "foo.bar.baz", extractDisplayName(probe)); 644 } 645 } 646 647 @Test testExtractFileName()648 public void testExtractFileName() throws Exception { 649 for (String probe : new String[] { 650 "foo", 651 "/foo", 652 "/sdcard/foo", 653 "foo.bar", 654 "/foo.bar", 655 "/sdcard/foo.bar", 656 }) { 657 assertEquals(probe, "foo", extractFileName(probe)); 658 } 659 } 660 661 @Test testExtractFileName_empty()662 public void testExtractFileName_empty() throws Exception { 663 for (String probe : new String[] { 664 "", 665 "/", 666 ".bar", 667 "/.bar", 668 "/sdcard/.bar", 669 }) { 670 assertEquals(probe, "", extractFileName(probe)); 671 } 672 } 673 674 @Test testExtractFileExtension()675 public void testExtractFileExtension() throws Exception { 676 for (String probe : new String[] { 677 ".bar", 678 "foo.bar", 679 "/.bar", 680 "/foo.bar", 681 "/sdcard/.bar", 682 "/sdcard/foo.bar", 683 "/sdcard/foo.baz.bar", 684 "/sdcard/foo..bar", 685 }) { 686 assertEquals(probe, "bar", extractFileExtension(probe)); 687 } 688 } 689 690 @Test testExtractFileExtension_none()691 public void testExtractFileExtension_none() throws Exception { 692 for (String probe : new String[] { 693 "", 694 "/", 695 "/sdcard/", 696 "bar", 697 "/bar", 698 "/sdcard/bar", 699 }) { 700 assertEquals(probe, null, extractFileExtension(probe)); 701 } 702 } 703 704 @Test testExtractFileExtension_empty()705 public void testExtractFileExtension_empty() throws Exception { 706 for (String probe : new String[] { 707 "foo.", 708 "/foo.", 709 "/sdcard/foo.", 710 }) { 711 assertEquals(probe, "", extractFileExtension(probe)); 712 } 713 } 714 715 @Test testSanitizeValues()716 public void testSanitizeValues() throws Exception { 717 final ContentValues values = new ContentValues(); 718 values.put(MediaColumns.RELATIVE_PATH, "path/in\0valid/data/"); 719 values.put(MediaColumns.DISPLAY_NAME, "inva\0lid"); 720 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ true); 721 assertEquals("path/in_valid/data/", values.get(MediaColumns.RELATIVE_PATH)); 722 assertEquals("inva_lid", values.get(MediaColumns.DISPLAY_NAME)); 723 } 724 725 @Test testSanitizeValues_Root()726 public void testSanitizeValues_Root() throws Exception { 727 final ContentValues values = new ContentValues(); 728 values.put(MediaColumns.RELATIVE_PATH, "/"); 729 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ true); 730 assertEquals("/", values.get(MediaColumns.RELATIVE_PATH)); 731 } 732 733 @Test testSanitizeValues_HiddenFile()734 public void testSanitizeValues_HiddenFile() throws Exception { 735 final String hiddenDirectoryPath = ".hiddenDirectory/"; 736 final String hiddenFileName = ".hiddenFile"; 737 final ContentValues values = new ContentValues(); 738 values.put(MediaColumns.RELATIVE_PATH, hiddenDirectoryPath); 739 values.put(MediaColumns.DISPLAY_NAME, hiddenFileName); 740 741 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ false); 742 assertEquals(hiddenDirectoryPath, values.get(MediaColumns.RELATIVE_PATH)); 743 assertEquals(hiddenFileName, values.get(MediaColumns.DISPLAY_NAME)); 744 745 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ true); 746 assertEquals("_" + hiddenDirectoryPath, values.get(MediaColumns.RELATIVE_PATH)); 747 assertEquals("_" + hiddenFileName, values.get(MediaColumns.DISPLAY_NAME)); 748 } 749 750 @Test testComputeDateExpires_None()751 public void testComputeDateExpires_None() throws Exception { 752 final ContentValues values = new ContentValues(); 753 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 754 755 FileUtils.computeDateExpires(values); 756 assertFalse(values.containsKey(MediaColumns.DATE_EXPIRES)); 757 } 758 759 @Test testComputeDateExpires_Pending_Set()760 public void testComputeDateExpires_Pending_Set() throws Exception { 761 final ContentValues values = new ContentValues(); 762 values.put(MediaColumns.IS_PENDING, 1); 763 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 764 765 FileUtils.computeDateExpires(values); 766 final long target = (System.currentTimeMillis() 767 + FileUtils.DEFAULT_DURATION_PENDING) / 1_000; 768 assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES)) 769 .isIn(Range.closed(target - 5, target + 5)); 770 } 771 772 @Test testComputeDateExpires_Pending_Clear()773 public void testComputeDateExpires_Pending_Clear() throws Exception { 774 final ContentValues values = new ContentValues(); 775 values.put(MediaColumns.IS_PENDING, 0); 776 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 777 778 FileUtils.computeDateExpires(values); 779 assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES)); 780 assertNull(values.get(MediaColumns.DATE_EXPIRES)); 781 } 782 783 @Test testComputeDateExpires_Trashed_Set()784 public void testComputeDateExpires_Trashed_Set() throws Exception { 785 final ContentValues values = new ContentValues(); 786 values.put(MediaColumns.IS_TRASHED, 1); 787 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 788 789 FileUtils.computeDateExpires(values); 790 final long target = (System.currentTimeMillis() 791 + FileUtils.DEFAULT_DURATION_TRASHED) / 1_000; 792 assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES)) 793 .isIn(Range.closed(target - 5, target + 5)); 794 } 795 796 @Test testComputeDateExpires_Trashed_Clear()797 public void testComputeDateExpires_Trashed_Clear() throws Exception { 798 final ContentValues values = new ContentValues(); 799 values.put(MediaColumns.IS_TRASHED, 0); 800 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 801 802 FileUtils.computeDateExpires(values); 803 assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES)); 804 assertNull(values.get(MediaColumns.DATE_EXPIRES)); 805 } 806 807 @Test testComputeDataFromValues_Trashed_trimFileName()808 public void testComputeDataFromValues_Trashed_trimFileName() throws Exception { 809 testComputeDataFromValues_withAction_trimFileName(MediaColumns.IS_TRASHED); 810 } 811 812 @Test testComputeDataFromValues_Pending_trimFileName()813 public void testComputeDataFromValues_Pending_trimFileName() throws Exception { 814 testComputeDataFromValues_withAction_trimFileName(MediaColumns.IS_PENDING); 815 } 816 817 @Test testGetTopLevelNoMedia_CurrentDir()818 public void testGetTopLevelNoMedia_CurrentDir() throws Exception { 819 File dirInDownload = getNewDirInDownload("testGetTopLevelNoMedia_CurrentDir"); 820 File nomedia = new File(dirInDownload, ".nomedia"); 821 assertTrue(nomedia.createNewFile()); 822 823 assertThat(FileUtils.getTopLevelNoMedia(dirInDownload)) 824 .isEqualTo(dirInDownload); 825 assertThat(FileUtils.getTopLevelNoMedia(new File(dirInDownload, "foo"))) 826 .isEqualTo(dirInDownload); 827 } 828 829 @Test testGetTopLevelNoMedia_CurrentNestedDir()830 public void testGetTopLevelNoMedia_CurrentNestedDir() throws Exception { 831 File topDirInDownload = getNewDirInDownload("testGetTopLevelNoMedia_CurrentNestedDir"); 832 833 File dirInTopDirInDownload = new File(topDirInDownload, "foo"); 834 assertTrue(dirInTopDirInDownload.mkdirs()); 835 File nomedia = new File(dirInTopDirInDownload, ".nomedia"); 836 assertTrue(nomedia.createNewFile()); 837 838 assertThat(FileUtils.getTopLevelNoMedia(dirInTopDirInDownload)) 839 .isEqualTo(dirInTopDirInDownload); 840 assertThat(FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo"))) 841 .isEqualTo(dirInTopDirInDownload); 842 } 843 844 @Test testGetTopLevelNoMedia_TopDir()845 public void testGetTopLevelNoMedia_TopDir() throws Exception { 846 File topDirInDownload = getNewDirInDownload("testGetTopLevelNoMedia_TopDir"); 847 File topNomedia = new File(topDirInDownload, ".nomedia"); 848 assertTrue(topNomedia.createNewFile()); 849 850 File dirInTopDirInDownload = new File(topDirInDownload, "foo"); 851 assertTrue(dirInTopDirInDownload.mkdirs()); 852 File nomedia = new File(dirInTopDirInDownload, ".nomedia"); 853 assertTrue(nomedia.createNewFile()); 854 855 assertThat(FileUtils.getTopLevelNoMedia(dirInTopDirInDownload)) 856 .isEqualTo(topDirInDownload); 857 assertThat(FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo"))) 858 .isEqualTo(topDirInDownload); 859 } 860 861 @Test testGetTopLevelNoMedia_NoDir()862 public void testGetTopLevelNoMedia_NoDir() throws Exception { 863 File topDirInDownload = getNewDirInDownload("testGetTopLevelNoMedia_NoDir"); 864 File dirInTopDirInDownload = new File(topDirInDownload, "foo"); 865 assertTrue(dirInTopDirInDownload.mkdirs()); 866 867 assertEquals(null, 868 FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo"))); 869 assertThat(FileUtils.getTopLevelNoMedia(dirInTopDirInDownload)) 870 .isNull(); 871 assertThat(FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo"))) 872 .isNull(); 873 } 874 875 @Test testShouldFileBeHidden()876 public void testShouldFileBeHidden() throws Exception { 877 File dir = getNewDirInDownload("testVisibleDirectory"); 878 879 // We don't create the files since shouldFileBeHidden needs to work even if the file has 880 // not been created yet. 881 882 File file = new File(dir, ".test-file"); 883 assertThat(FileUtils.shouldFileBeHidden(file)).isTrue(); 884 885 File hiddenFile = new File(dir, ".hidden-file"); 886 assertThat(FileUtils.shouldFileBeHidden(hiddenFile)).isTrue(); 887 } 888 889 @Test testShouldFileBeHidden_hiddenParent()890 public void testShouldFileBeHidden_hiddenParent() throws Exception { 891 File hiddenDirName = getNewDirInDownload(".testDirectory"); 892 893 // We don't create the file since shouldFileBeHidden needs to work even if the file has 894 // not been created yet. 895 896 File fileInHiddenParent = new File(hiddenDirName, "testDirectory.txt"); 897 assertThat(FileUtils.shouldFileBeHidden(fileInHiddenParent)).isTrue(); 898 } 899 900 // Visibility of default dirs is tested in ModernMediaScannerTest#testVisibleDefaultFolders. 901 @Test testShouldDirBeHidden()902 public void testShouldDirBeHidden() throws Exception { 903 final File root = new File("storage/emulated/0"); 904 assertThat(FileUtils.shouldDirBeHidden(root)).isFalse(); 905 906 // We don't create the dirs since shouldDirBeHidden needs to work even if the dir has 907 // not been created yet. 908 909 File visibleDir = new File(mTestDownloadDir, "testDirectory"); 910 assertThat(FileUtils.shouldDirBeHidden(visibleDir)).isFalse(); 911 912 File hiddenDir = new File(mTestDownloadDir, ".testDirectory"); 913 assertThat(FileUtils.shouldDirBeHidden(hiddenDir)).isTrue(); 914 } 915 916 @Test testShouldDirBeHidden_hiddenParent()917 public void testShouldDirBeHidden_hiddenParent() throws Exception { 918 File hiddenDirName = getNewDirInDownload(".testDirectory"); 919 920 // We don't create the dirs since shouldDirBeHidden needs to work even if the dir has 921 // not been created yet. 922 923 File dirInHiddenParent = new File(hiddenDirName, "testDirectory"); 924 assertThat(FileUtils.shouldDirBeHidden(dirInHiddenParent)).isTrue(); 925 } 926 assertDirectoryHidden(File file)927 private static void assertDirectoryHidden(File file) { 928 assertTrue(file.getAbsolutePath(), isDirectoryHidden(file)); 929 } 930 assertDirectoryNotHidden(File file)931 private static void assertDirectoryNotHidden(File file) { 932 assertFalse(file.getAbsolutePath(), isDirectoryHidden(file)); 933 } 934 935 // Visibility of default dirs is tested in ModernMediaScannerTest#testVisibleDefaultFolders. 936 @Test testIsDirectoryHidden()937 public void testIsDirectoryHidden() throws Exception { 938 for (String prefix : new String[] { 939 "/storage/emulated/0", 940 "/storage/0000-0000", 941 }) { 942 assertDirectoryNotHidden(new File(prefix)); 943 assertDirectoryNotHidden(new File(prefix + "/meow")); 944 945 assertDirectoryHidden(new File(prefix + "/.meow")); 946 } 947 948 final File nomediaFile = new File("storage/emulated/0/Download/meow", ".nomedia"); 949 try { 950 assertTrue(nomediaFile.getParentFile().mkdirs()); 951 assertTrue(nomediaFile.createNewFile()); 952 953 assertDirectoryHidden(nomediaFile.getParentFile()); 954 955 assertTrue(nomediaFile.delete()); 956 957 assertDirectoryNotHidden(nomediaFile.getParentFile()); 958 } finally { 959 nomediaFile.delete(); 960 nomediaFile.getParentFile().delete(); 961 } 962 } 963 964 @Test testIsDirectoryHidden_downloadDirectory()965 public void testIsDirectoryHidden_downloadDirectory() throws Exception { 966 File visibleDir = getNewDirInDownload("testDirectory"); 967 assertDirectoryNotHidden(visibleDir); 968 969 File hiddenDirName = getNewDirInDownload(".testDirectory"); 970 assertDirectoryHidden(hiddenDirName); 971 972 File hiddenDirNomedia = getNewDirInDownload("testDirectory2"); 973 File nomedia = new File(hiddenDirNomedia, ".nomedia"); 974 assertThat(nomedia.createNewFile()).isTrue(); 975 assertDirectoryHidden(hiddenDirNomedia); 976 } 977 978 @Test testIsFileHidden()979 public void testIsFileHidden() throws Exception { 980 assertFalse(isFileHidden( 981 new File("/storage/emulated/0/DCIM/IMG1024.JPG"))); 982 assertFalse(isFileHidden( 983 new File("/storage/emulated/0/DCIM/.pending-1577836800-IMG1024.JPG"))); 984 assertFalse(isFileHidden( 985 new File("/storage/emulated/0/DCIM/.trashed-1577836800-IMG1024.JPG"))); 986 assertTrue(isFileHidden( 987 new File("/storage/emulated/0/DCIM/.IMG1024.JPG"))); 988 } 989 990 @Test testDirectoryDirty()991 public void testDirectoryDirty() throws Exception { 992 File dirInDownload = getNewDirInDownload("testDirectoryDirty"); 993 994 // Directory without nomedia is not dirty 995 assertFalse(FileUtils.isDirectoryDirty(dirInDownload)); 996 997 // Creating an empty .nomedia file makes directory dirty 998 File nomedia = new File(dirInDownload, ".nomedia"); 999 assertTrue(nomedia.createNewFile()); 1000 assertTrue(FileUtils.isDirectoryDirty(dirInDownload)); 1001 1002 // Marking as clean with a .nomedia file works 1003 FileUtils.setDirectoryDirty(dirInDownload, false); 1004 assertFalse(FileUtils.isDirectoryDirty(dirInDownload)); 1005 1006 // Marking as dirty with a .nomedia file works 1007 FileUtils.setDirectoryDirty(dirInDownload, true); 1008 assertTrue(FileUtils.isDirectoryDirty(dirInDownload)); 1009 1010 // Test case-insensitivity 1011 File dirInDownloadDifferentCase = new File(mTestDownloadDir, "TeStDirEctoRYdirTy"); 1012 assertTrue(FileUtils.isDirectoryDirty(dirInDownloadDifferentCase)); 1013 } 1014 1015 @Test testDirectoryDirty_noMediaDirectory()1016 public void testDirectoryDirty_noMediaDirectory() throws Exception { 1017 File dirInDownload = getNewDirInDownload("testDirectoryDirty"); 1018 1019 // Directory without nomedia is clean 1020 assertFalse(FileUtils.isDirectoryDirty(dirInDownload)); 1021 1022 // Creating a .nomedia directory makes directory dirty 1023 File nomedia = new File(dirInDownload, ".nomedia"); 1024 assertTrue(nomedia.mkdir()); 1025 assertTrue(FileUtils.isDirectoryDirty(dirInDownload)); 1026 1027 // Marking as clean with a .nomedia directory has no effect 1028 FileUtils.setDirectoryDirty(dirInDownload, false); 1029 assertTrue(FileUtils.isDirectoryDirty(dirInDownload)); 1030 } 1031 1032 @Test testDirectoryDirty_nullDir()1033 public void testDirectoryDirty_nullDir() throws Exception { 1034 assertThat(FileUtils.isDirectoryDirty(null)).isFalse(); 1035 } 1036 1037 @Test testExtractPathOwnerPackageName()1038 public void testExtractPathOwnerPackageName() { 1039 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data/foo")) 1040 .isEqualTo("foo"); 1041 assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/data/foo")) 1042 .isEqualTo("foo"); 1043 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb/foo")) 1044 .isEqualTo("foo"); 1045 assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/obb/foo")) 1046 .isEqualTo("foo"); 1047 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media/foo")) 1048 .isEqualTo("foo"); 1049 assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/media/foo")) 1050 .isEqualTo("foo"); 1051 assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/data/foo")) 1052 .isEqualTo("foo"); 1053 assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/obb/foo")) 1054 .isEqualTo("foo"); 1055 assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media/foo")) 1056 .isEqualTo("foo"); 1057 assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/android/media/foo")) 1058 .isEqualTo("foo"); 1059 1060 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data")).isNull(); 1061 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb")).isNull(); 1062 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media")).isNull(); 1063 assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media")).isNull(); 1064 assertThat(extractPathOwnerPackageName("/storage/emulated/0/Pictures/foo")).isNull(); 1065 assertThat(extractPathOwnerPackageName("Android/data")).isNull(); 1066 assertThat(extractPathOwnerPackageName("Android/obb")).isNull(); 1067 } 1068 1069 @Test testExtractOwnerPackageNameFromRelativePath()1070 public void testExtractOwnerPackageNameFromRelativePath() { 1071 assertThat(extractOwnerPackageNameFromRelativePath("Android/data/foo")).isEqualTo("foo"); 1072 assertThat(extractOwnerPackageNameFromRelativePath("Android/obb/foo")).isEqualTo("foo"); 1073 assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo")).isEqualTo("foo"); 1074 assertThat(extractOwnerPackageNameFromRelativePath("Android/media/foo.com/files")) 1075 .isEqualTo("foo.com"); 1076 1077 assertThat(extractOwnerPackageNameFromRelativePath("/storage/emulated/0/Android/data/foo")) 1078 .isNull(); 1079 assertThat(extractOwnerPackageNameFromRelativePath("Android/data")).isNull(); 1080 assertThat(extractOwnerPackageNameFromRelativePath("Android/obb")).isNull(); 1081 assertThat(extractOwnerPackageNameFromRelativePath("Android/media")).isNull(); 1082 assertThat(extractOwnerPackageNameFromRelativePath("Pictures/foo")).isNull(); 1083 } 1084 1085 @Test testIsDataOrObbPath()1086 public void testIsDataOrObbPath() { 1087 assertThat(isDataOrObbPath("/storage/emulated/0/Android/data")).isTrue(); 1088 assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb")).isTrue(); 1089 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data")).isTrue(); 1090 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb")).isTrue(); 1091 1092 assertThat(isDataOrObbPath("/storage/emulated/0/Android/data/foo")).isFalse(); 1093 assertThat(isDataOrObbPath("/storage/emulated/0/Android/obb/foo")).isFalse(); 1094 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/data/foo")).isFalse(); 1095 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obb/foo")).isFalse(); 1096 assertThat(isDataOrObbPath("/storage/emulated/10/Android/obb/foo")).isFalse(); 1097 assertThat(isDataOrObbPath("/storage/emulated//Android/obb/foo")).isFalse(); 1098 assertThat(isDataOrObbPath("/storage/emulated//Android/obb")).isFalse(); 1099 assertThat(isDataOrObbPath("/storage/emulated/0//Android/obb")).isFalse(); 1100 assertThat(isDataOrObbPath("/storage/emulated/0//Android/obb/foo")).isFalse(); 1101 assertThat(isDataOrObbPath("/storage/emulated/0/Android/")).isFalse(); 1102 assertThat(isDataOrObbPath("/storage/emulated/0/Android/media/")).isFalse(); 1103 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/media/")).isFalse(); 1104 assertThat(isDataOrObbPath("/storage/emulated/0/Pictures/")).isFalse(); 1105 assertThat(isDataOrObbPath("/storage/ABCD-1234/Android/obbfoo")).isFalse(); 1106 assertThat(isDataOrObbPath("/storage/emulated/0/Android/datafoo")).isFalse(); 1107 assertThat(isDataOrObbPath("Android/")).isFalse(); 1108 assertThat(isDataOrObbPath("Android/media/")).isFalse(); 1109 } 1110 1111 @Test testIsDataOrObbRelativePath()1112 public void testIsDataOrObbRelativePath() { 1113 assertThat(isDataOrObbRelativePath("Android/data")).isTrue(); 1114 assertThat(isDataOrObbRelativePath("Android/obb")).isTrue(); 1115 assertThat(isDataOrObbRelativePath("Android/data/foo")).isTrue(); 1116 assertThat(isDataOrObbRelativePath("Android/obb/foo")).isTrue(); 1117 1118 assertThat(isDataOrObbRelativePath("/storage/emulated/0/Android/data")).isFalse(); 1119 assertThat(isDataOrObbRelativePath("Android/")).isFalse(); 1120 assertThat(isDataOrObbRelativePath("Android/media/")).isFalse(); 1121 assertThat(isDataOrObbRelativePath("Pictures/")).isFalse(); 1122 } 1123 1124 @Test testIsObbOrChildRelativePath()1125 public void testIsObbOrChildRelativePath() { 1126 assertThat(isObbOrChildRelativePath("Android/obb")).isTrue(); 1127 assertThat(isObbOrChildRelativePath("Android/obb/")).isTrue(); 1128 assertThat(isObbOrChildRelativePath("Android/obb/foo.com")).isTrue(); 1129 1130 assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/obb")).isFalse(); 1131 assertThat(isObbOrChildRelativePath("/storage/emulated/0/Android/")).isFalse(); 1132 assertThat(isObbOrChildRelativePath("Android/")).isFalse(); 1133 assertThat(isObbOrChildRelativePath("Android/media/")).isFalse(); 1134 assertThat(isObbOrChildRelativePath("Pictures/")).isFalse(); 1135 assertThat(isObbOrChildRelativePath("Android/obbfoo")).isFalse(); 1136 assertThat(isObbOrChildRelativePath("Android/data")).isFalse(); 1137 } 1138 getNewDirInDownload(String name)1139 private File getNewDirInDownload(String name) { 1140 File file = new File(mTestDownloadDir, name); 1141 assertTrue(file.mkdir()); 1142 return file; 1143 } 1144 touch(File dir, String name)1145 private static File touch(File dir, String name) throws IOException { 1146 final File res = new File(dir, name); 1147 res.createNewFile(); 1148 return res; 1149 } 1150 assertNameEquals(String expected, File actual)1151 private static void assertNameEquals(String expected, File actual) { 1152 assertEquals(expected, actual.getName()); 1153 } 1154 assertDirContents(String... expected)1155 private void assertDirContents(String... expected) { 1156 final HashSet<String> expectedSet = new HashSet<>(Arrays.asList(expected)); 1157 String[] actual = mDeleteTarget.list(); 1158 if (actual == null) actual = new String[0]; 1159 1160 assertEquals( 1161 "Expected " + Arrays.toString(expected) + " but actual " + Arrays.toString(actual), 1162 expected.length, actual.length); 1163 for (String actualFile : actual) { 1164 assertTrue("Unexpected actual file " + actualFile, expectedSet.contains(actualFile)); 1165 } 1166 } 1167 createExtremeFileName(String prefix, String extension)1168 public static String createExtremeFileName(String prefix, String extension) { 1169 // create extreme long file name 1170 final int prefixLength = prefix.length(); 1171 final int extensionLength = extension.length(); 1172 StringBuilder str = new StringBuilder(prefix); 1173 for (int i = 0; i < (MAX_FILENAME_BYTES - prefixLength - extensionLength); i++) { 1174 str.append(i % 10); 1175 } 1176 return str.append(extension).toString(); 1177 } 1178 testComputeDataFromValues_withAction_trimFileName(String columnKey)1179 private void testComputeDataFromValues_withAction_trimFileName(String columnKey) { 1180 final String originalName = createExtremeFileName("test", ".jpg"); 1181 final String volumePath = "/storage/emulated/0/"; 1182 final ContentValues values = new ContentValues(); 1183 values.put(columnKey, 1); 1184 values.put(MediaColumns.RELATIVE_PATH, "DCIM/My Vacation/"); 1185 values.put(MediaColumns.DATE_EXPIRES, 1577836800L); 1186 values.put(MediaColumns.DISPLAY_NAME, originalName); 1187 1188 FileUtils.computeDataFromValues(values, new File(volumePath), false /* isForFuse */); 1189 1190 final String data = values.getAsString(MediaColumns.DATA); 1191 final String result = FileUtils.extractDisplayName(data); 1192 // after adding the prefix .pending-timestamp or .trashed-timestamp, 1193 // the largest length of the file name is MAX_FILENAME_BYTES 255 1194 assertThat(result.length()).isAtMost(MAX_FILENAME_BYTES); 1195 assertThat(result).isNotEqualTo(originalName); 1196 } 1197 1198 @Test testIsExternalMediaDirectory()1199 public void testIsExternalMediaDirectory() throws Exception { 1200 for (String prefix : new String[] { 1201 "/storage/emulated/0/", 1202 "/storage/0000-0000/", 1203 }) { 1204 assertTrue(isExternalMediaDirectory(prefix + "Android/media/foo.jpg", null)); 1205 assertTrue(isExternalMediaDirectory(prefix + "Android/media/foo.jpg", "")); 1206 assertTrue(isExternalMediaDirectory(prefix + "Android/mEdia/foo.jpg", "")); 1207 assertFalse(isExternalMediaDirectory(prefix + "Android/data/foo.jpg", "")); 1208 assertTrue(isExternalMediaDirectory(prefix + "Android/media/foo.jpg", "AppClone")); 1209 assertTrue(isExternalMediaDirectory(prefix + "android/mEdia/foo.jpg", "AppClone")); 1210 assertTrue(isExternalMediaDirectory(prefix + "AppClone/Android/media/foo.jpg", "AppClone")); 1211 assertTrue(isExternalMediaDirectory(prefix + "AppClone/Android/mEdia/foo.jpg", "AppClone")); 1212 assertTrue(isExternalMediaDirectory(prefix + "Appclone/Android/mEdia/foo.jpg", "AppClone")); 1213 assertFalse(isExternalMediaDirectory(prefix + "AppClone/Android/media/foo.jpg", null)); 1214 assertFalse(isExternalMediaDirectory(prefix + "AppClone/Android/mEdia/foo.jpg", null)); 1215 assertFalse(isExternalMediaDirectory(prefix + "AppClone/Android/media/foo.jpg", "")); 1216 assertFalse(isExternalMediaDirectory(prefix + "AppClone/Android/media/foo.jpg", "NotAppClone")); 1217 } 1218 } 1219 1220 @Test testToAndFromFuseFile()1221 public void testToAndFromFuseFile() throws Exception { 1222 final File fuseFilePrimary = new File("/mnt/user/0/emulated/0/foo"); 1223 final File fuseFileSecondary = new File("/mnt/user/0/0000-0000/foo"); 1224 1225 final File lowerFsFilePrimary = new File("/storage/emulated/0/foo"); 1226 final File lowerFsFileSecondary = new File("/storage/0000-0000/foo"); 1227 1228 final File unexpectedFile = new File("/mnt/pass_through/0/emulated/0/foo"); 1229 1230 assertThat(fromFuseFile(fuseFilePrimary)).isEqualTo(lowerFsFilePrimary); 1231 assertThat(fromFuseFile(fuseFileSecondary)).isEqualTo(lowerFsFileSecondary); 1232 assertThat(fromFuseFile(lowerFsFilePrimary)).isEqualTo(lowerFsFilePrimary); 1233 1234 assertThat(toFuseFile(lowerFsFilePrimary)).isEqualTo(fuseFilePrimary); 1235 assertThat(toFuseFile(lowerFsFileSecondary)).isEqualTo(fuseFileSecondary); 1236 assertThat(toFuseFile(fuseFilePrimary)).isEqualTo(fuseFilePrimary); 1237 1238 assertThat(toFuseFile(unexpectedFile)).isEqualTo(unexpectedFile); 1239 assertThat(fromFuseFile(unexpectedFile)).isEqualTo(unexpectedFile); 1240 } 1241 1242 @Test testComputeValuesFromData()1243 public void testComputeValuesFromData() { 1244 final ContentValues values = new ContentValues(); 1245 values.put(MediaColumns.DATA, "/storage/emulated/0/Pictures/foo.jpg"); 1246 1247 FileUtils.computeValuesFromData(values, false); 1248 1249 assertEquals("external_primary", values.getAsString(MediaColumns.VOLUME_NAME)); 1250 assertEquals("Pictures/", values.getAsString(MediaColumns.RELATIVE_PATH)); 1251 assertEquals(0, (int) values.getAsInteger(MediaColumns.IS_TRASHED)); 1252 assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES)); 1253 assertNull(values.get(MediaColumns.DATE_EXPIRES)); 1254 assertEquals("foo.jpg", values.getAsString(MediaColumns.DISPLAY_NAME)); 1255 assertTrue(values.containsKey(MediaColumns.BUCKET_DISPLAY_NAME)); 1256 assertEquals("Pictures", values.get(MediaColumns.BUCKET_DISPLAY_NAME)); 1257 } 1258 1259 @Test testComputeValuesFromData_withTopLevelFile()1260 public void testComputeValuesFromData_withTopLevelFile() { 1261 final ContentValues values = new ContentValues(); 1262 values.put(MediaColumns.DATA, "/storage/emulated/0/foo.jpg"); 1263 1264 FileUtils.computeValuesFromData(values, false); 1265 1266 assertEquals("external_primary", values.getAsString(MediaColumns.VOLUME_NAME)); 1267 assertEquals("/", values.getAsString(MediaColumns.RELATIVE_PATH)); 1268 assertEquals(0, (int) values.getAsInteger(MediaColumns.IS_TRASHED)); 1269 assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES)); 1270 assertNull(values.get(MediaColumns.DATE_EXPIRES)); 1271 assertEquals("foo.jpg", values.getAsString(MediaColumns.DISPLAY_NAME)); 1272 assertTrue(values.containsKey(MediaColumns.BUCKET_DISPLAY_NAME)); 1273 assertNull(values.get(MediaColumns.BUCKET_DISPLAY_NAME)); 1274 } 1275 1276 @Test testComputeDataFromValuesForValidPath_success()1277 public void testComputeDataFromValuesForValidPath_success() { 1278 final ContentValues values = new ContentValues(); 1279 values.put(MediaColumns.RELATIVE_PATH, "Android/media/com.example"); 1280 values.put(MediaColumns.DISPLAY_NAME, "./../../abc.txt"); 1281 1282 FileUtils.computeDataFromValues(values, new File("/storage/emulated/0"), false); 1283 1284 assertThat(values.getAsString(MediaColumns.DATA)).isEqualTo( 1285 "/storage/emulated/0/Android/abc.txt"); 1286 } 1287 1288 @Test testComputeDataFromValuesForInvalidPath_throwsIllegalArgumentException()1289 public void testComputeDataFromValuesForInvalidPath_throwsIllegalArgumentException() { 1290 final ContentValues values = new ContentValues(); 1291 values.put(MediaColumns.RELATIVE_PATH, "\0"); 1292 values.put(MediaColumns.DISPLAY_NAME, "./../../abc.txt"); 1293 1294 assertThrows(IllegalArgumentException.class, 1295 () -> FileUtils.computeDataFromValues(values, new File("/storage/emulated/0"), 1296 false)); 1297 } 1298 1299 @Test testComputeAudioTypeValuesFromData()1300 public void testComputeAudioTypeValuesFromData() { 1301 testComputeAudioTypeValuesFromData("/storage/emulated/0/Ringtones/a.mp3", 1302 AudioColumns.IS_RINGTONE); 1303 testComputeAudioTypeValuesFromData("/storage/emulated/0/Notifications/a.mp3", 1304 AudioColumns.IS_NOTIFICATION); 1305 testComputeAudioTypeValuesFromData("/storage/emulated/0/Alarms/a.mp3", 1306 AudioColumns.IS_ALARM); 1307 testComputeAudioTypeValuesFromData("/storage/emulated/0/Podcasts/a.mp3", 1308 AudioColumns.IS_PODCAST); 1309 testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/a.mp3", 1310 AudioColumns.IS_AUDIOBOOK); 1311 testComputeAudioTypeValuesFromData("/storage/emulated/0/Recordings/a.mp3", 1312 AudioColumns.IS_RECORDING); 1313 testComputeAudioTypeValuesFromData("/storage/emulated/0/Music/a.mp3", 1314 AudioColumns.IS_MUSIC); 1315 1316 // Categorized as music if it doesn't match any other category 1317 testComputeAudioTypeValuesFromData("/storage/emulated/0/a/a.mp3", AudioColumns.IS_MUSIC); 1318 1319 // All matches are case-insensitive 1320 testComputeAudioTypeValuesFromData("/storage/emulated/0/ringtones/a.mp3", 1321 AudioColumns.IS_RINGTONE); 1322 testComputeAudioTypeValuesFromData("/storage/emulated/0/a/ringtones/a.mp3", 1323 AudioColumns.IS_RINGTONE); 1324 } 1325 1326 @Test testComputeAudioTypeValuesFromData_multiple()1327 public void testComputeAudioTypeValuesFromData_multiple() { 1328 testComputeAudioTypeValuesFromData("/storage/emulated/0/Ringtones/Recordings/a.mp3", 1329 Arrays.asList(AudioColumns.IS_RINGTONE, AudioColumns.IS_RECORDING)); 1330 testComputeAudioTypeValuesFromData("/storage/emulated/0/Alarms/Notifications/a.mp3", 1331 Arrays.asList(AudioColumns.IS_ALARM, AudioColumns.IS_NOTIFICATION)); 1332 testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/Podcasts/a.mp3", 1333 Arrays.asList(AudioColumns.IS_AUDIOBOOK, AudioColumns.IS_PODCAST)); 1334 testComputeAudioTypeValuesFromData("/storage/emulated/0/Audiobooks/Ringtones/a.mp3", 1335 Arrays.asList(AudioColumns.IS_AUDIOBOOK, AudioColumns.IS_RINGTONE)); 1336 testComputeAudioTypeValuesFromData("/storage/emulated/0/Music/Ringtones/a.mp3", 1337 Arrays.asList(AudioColumns.IS_MUSIC, AudioColumns.IS_RINGTONE)); 1338 } 1339 testComputeAudioTypeValuesFromData(String path, String expectedColumn)1340 private void testComputeAudioTypeValuesFromData(String path, String expectedColumn) { 1341 testComputeAudioTypeValuesFromData(path, Collections.singletonList(expectedColumn)); 1342 } 1343 testComputeAudioTypeValuesFromData(String path, List<String> expectedColumns)1344 private void testComputeAudioTypeValuesFromData(String path, List<String> expectedColumns) { 1345 final ContentValues values = new ContentValues(); 1346 FileUtils.computeAudioTypeValuesFromData(path, values::put); 1347 1348 for (String column : FileUtils.sAudioTypes.values()) { 1349 if (expectedColumns.contains(column)) { 1350 assertWithMessage("Expected " + column + " to be set for " + path) 1351 .that(values.get(column)).isEqualTo(1); 1352 } else { 1353 assertWithMessage("Expected " + column + " to be unset for " + path) 1354 .that(values.get(column)).isEqualTo(0); 1355 } 1356 } 1357 } 1358 } 1359