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