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