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