• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.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_ACCMODE;
27 import static android.system.OsConstants.O_APPEND;
28 import static android.system.OsConstants.O_CLOEXEC;
29 import static android.system.OsConstants.O_CREAT;
30 import static android.system.OsConstants.O_NOFOLLOW;
31 import static android.system.OsConstants.O_RDONLY;
32 import static android.system.OsConstants.O_RDWR;
33 import static android.system.OsConstants.O_TRUNC;
34 import static android.system.OsConstants.O_WRONLY;
35 import static android.system.OsConstants.R_OK;
36 import static android.system.OsConstants.S_IRWXG;
37 import static android.system.OsConstants.S_IRWXU;
38 import static android.system.OsConstants.W_OK;
39 
40 import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
41 import static com.android.providers.media.util.DatabaseUtils.getAsLong;
42 import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
43 import static com.android.providers.media.util.Logging.TAG;
44 
45 import android.content.ClipDescription;
46 import android.content.ContentValues;
47 import android.content.Context;
48 import android.content.pm.PackageManager;
49 import android.net.Uri;
50 import android.os.Build;
51 import android.os.Environment;
52 import android.os.ParcelFileDescriptor;
53 import android.os.UserHandle;
54 import android.os.SystemProperties;
55 import android.os.storage.StorageManager;
56 import android.os.storage.StorageVolume;
57 import android.provider.MediaStore;
58 import android.provider.MediaStore.MediaColumns;
59 import android.system.ErrnoException;
60 import android.system.Os;
61 import android.system.OsConstants;
62 import android.text.TextUtils;
63 import android.text.format.DateUtils;
64 import android.util.Log;
65 import android.webkit.MimeTypeMap;
66 
67 import androidx.annotation.NonNull;
68 import androidx.annotation.Nullable;
69 import androidx.annotation.VisibleForTesting;
70 
71 import com.android.modules.utils.build.SdkLevel;
72 
73 import java.io.File;
74 import java.io.FileDescriptor;
75 import java.io.FileNotFoundException;
76 import java.io.IOException;
77 import java.io.InputStream;
78 import java.io.OutputStream;
79 import java.nio.charset.StandardCharsets;
80 import java.nio.file.FileVisitResult;
81 import java.nio.file.FileVisitor;
82 import java.nio.file.Files;
83 import java.nio.file.NoSuchFileException;
84 import java.nio.file.Path;
85 import java.nio.file.attribute.BasicFileAttributes;
86 import java.util.ArrayList;
87 import java.util.Arrays;
88 import java.util.Collection;
89 import java.util.Comparator;
90 import java.util.Iterator;
91 import java.util.Locale;
92 import java.util.Objects;
93 import java.util.Optional;
94 import java.util.function.Consumer;
95 import java.util.regex.Matcher;
96 import java.util.regex.Pattern;
97 
98 public class FileUtils {
99     // Even though vfat allows 255 UCS-2 chars, we might eventually write to
100     // ext4 through a FUSE layer, so use that limit.
101     @VisibleForTesting
102     static final int MAX_FILENAME_BYTES = 255;
103 
104     /**
105      * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)}
106      * which adds security features like {@link OsConstants#O_CLOEXEC} and
107      * {@link OsConstants#O_NOFOLLOW}.
108      */
openSafely(@onNull File file, int pfdFlags)109     public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags)
110             throws FileNotFoundException {
111         final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW;
112         try {
113             final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags,
114                     S_IRWXU | S_IRWXG);
115             try {
116                 return ParcelFileDescriptor.dup(fd);
117             } finally {
118                 closeQuietly(fd);
119             }
120         } catch (IOException | ErrnoException e) {
121             throw new FileNotFoundException(e.getMessage());
122         }
123     }
124 
closeQuietly(@ullable AutoCloseable closeable)125     public static void closeQuietly(@Nullable AutoCloseable closeable) {
126         android.os.FileUtils.closeQuietly(closeable);
127     }
128 
closeQuietly(@ullable FileDescriptor fd)129     public static void closeQuietly(@Nullable FileDescriptor fd) {
130         if (fd == null) return;
131         try {
132             Os.close(fd);
133         } catch (ErrnoException ignored) {
134         }
135     }
136 
copy(@onNull InputStream in, @NonNull OutputStream out)137     public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
138         return android.os.FileUtils.copy(in, out);
139     }
140 
buildPath(File base, String... segments)141     public static File buildPath(File base, String... segments) {
142         File cur = base;
143         for (String segment : segments) {
144             if (cur == null) {
145                 cur = new File(segment);
146             } else {
147                 cur = new File(cur, segment);
148             }
149         }
150         return cur;
151     }
152 
153     /**
154      * Delete older files in a directory until only those matching the given
155      * constraints remain.
156      *
157      * @param minCount Always keep at least this many files.
158      * @param minAgeMs Always keep files younger than this age, in milliseconds.
159      * @return if any files were deleted.
160      */
deleteOlderFiles(File dir, int minCount, long minAgeMs)161     public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) {
162         if (minCount < 0 || minAgeMs < 0) {
163             throw new IllegalArgumentException("Constraints must be positive or 0");
164         }
165 
166         final File[] files = dir.listFiles();
167         if (files == null) return false;
168 
169         // Sort with newest files first
170         Arrays.sort(files, new Comparator<File>() {
171             @Override
172             public int compare(File lhs, File rhs) {
173                 return Long.compare(rhs.lastModified(), lhs.lastModified());
174             }
175         });
176 
177         // Keep at least minCount files
178         boolean deleted = false;
179         for (int i = minCount; i < files.length; i++) {
180             final File file = files[i];
181 
182             // Keep files newer than minAgeMs
183             final long age = System.currentTimeMillis() - file.lastModified();
184             if (age > minAgeMs) {
185                 if (file.delete()) {
186                     Log.d(TAG, "Deleted old file " + file);
187                     deleted = true;
188                 }
189             }
190         }
191         return deleted;
192     }
193 
194     /**
195      * Shamelessly borrowed from {@code android.os.FileUtils}.
196      */
translateModeStringToPosix(String mode)197     public static int translateModeStringToPosix(String mode) {
198         // Sanity check for invalid chars
199         for (int i = 0; i < mode.length(); i++) {
200             switch (mode.charAt(i)) {
201                 case 'r':
202                 case 'w':
203                 case 't':
204                 case 'a':
205                     break;
206                 default:
207                     throw new IllegalArgumentException("Bad mode: " + mode);
208             }
209         }
210 
211         int res = 0;
212         if (mode.startsWith("rw")) {
213             res = O_RDWR | O_CREAT;
214         } else if (mode.startsWith("w")) {
215             res = O_WRONLY | O_CREAT;
216         } else if (mode.startsWith("r")) {
217             res = O_RDONLY;
218         } else {
219             throw new IllegalArgumentException("Bad mode: " + mode);
220         }
221         if (mode.indexOf('t') != -1) {
222             res |= O_TRUNC;
223         }
224         if (mode.indexOf('a') != -1) {
225             res |= O_APPEND;
226         }
227         return res;
228     }
229 
230     /**
231      * Shamelessly borrowed from {@code android.os.FileUtils}.
232      */
translateModePosixToString(int mode)233     public static String translateModePosixToString(int mode) {
234         String res = "";
235         if ((mode & O_ACCMODE) == O_RDWR) {
236             res = "rw";
237         } else if ((mode & O_ACCMODE) == O_WRONLY) {
238             res = "w";
239         } else if ((mode & O_ACCMODE) == O_RDONLY) {
240             res = "r";
241         } else {
242             throw new IllegalArgumentException("Bad mode: " + mode);
243         }
244         if ((mode & O_TRUNC) == O_TRUNC) {
245             res += "t";
246         }
247         if ((mode & O_APPEND) == O_APPEND) {
248             res += "a";
249         }
250         return res;
251     }
252 
253     /**
254      * Shamelessly borrowed from {@code android.os.FileUtils}.
255      */
translateModePosixToPfd(int mode)256     public static int translateModePosixToPfd(int mode) {
257         int res = 0;
258         if ((mode & O_ACCMODE) == O_RDWR) {
259             res = MODE_READ_WRITE;
260         } else if ((mode & O_ACCMODE) == O_WRONLY) {
261             res = MODE_WRITE_ONLY;
262         } else if ((mode & O_ACCMODE) == O_RDONLY) {
263             res = MODE_READ_ONLY;
264         } else {
265             throw new IllegalArgumentException("Bad mode: " + mode);
266         }
267         if ((mode & O_CREAT) == O_CREAT) {
268             res |= MODE_CREATE;
269         }
270         if ((mode & O_TRUNC) == O_TRUNC) {
271             res |= MODE_TRUNCATE;
272         }
273         if ((mode & O_APPEND) == O_APPEND) {
274             res |= MODE_APPEND;
275         }
276         return res;
277     }
278 
279     /**
280      * Shamelessly borrowed from {@code android.os.FileUtils}.
281      */
translateModePfdToPosix(int mode)282     public static int translateModePfdToPosix(int mode) {
283         int res = 0;
284         if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
285             res = O_RDWR;
286         } else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
287             res = O_WRONLY;
288         } else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) {
289             res = O_RDONLY;
290         } else {
291             throw new IllegalArgumentException("Bad mode: " + mode);
292         }
293         if ((mode & MODE_CREATE) == MODE_CREATE) {
294             res |= O_CREAT;
295         }
296         if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) {
297             res |= O_TRUNC;
298         }
299         if ((mode & MODE_APPEND) == MODE_APPEND) {
300             res |= O_APPEND;
301         }
302         return res;
303     }
304 
305     /**
306      * Shamelessly borrowed from {@code android.os.FileUtils}.
307      */
translateModeAccessToPosix(int mode)308     public static int translateModeAccessToPosix(int mode) {
309         if (mode == F_OK) {
310             // There's not an exact mapping, so we attempt a read-only open to
311             // determine if a file exists
312             return O_RDONLY;
313         } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) {
314             return O_RDWR;
315         } else if ((mode & R_OK) == R_OK) {
316             return O_RDONLY;
317         } else if ((mode & W_OK) == W_OK) {
318             return O_WRONLY;
319         } else {
320             throw new IllegalArgumentException("Bad mode: " + mode);
321         }
322     }
323 
324     /**
325      * Test if a file lives under the given directory, either as a direct child
326      * or a distant grandchild.
327      * <p>
328      * Both files <em>must</em> have been resolved using
329      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
330      * attacks.
331      *
332      * @hide
333      */
contains(File[] dirs, File file)334     public static boolean contains(File[] dirs, File file) {
335         for (File dir : dirs) {
336             if (contains(dir, file)) {
337                 return true;
338             }
339         }
340         return false;
341     }
342 
343     /** {@hide} */
contains(Collection<File> dirs, File file)344     public static boolean contains(Collection<File> dirs, File file) {
345         for (File dir : dirs) {
346             if (contains(dir, file)) {
347                 return true;
348             }
349         }
350         return false;
351     }
352 
353     /**
354      * Test if a file lives under the given directory, either as a direct child
355      * or a distant grandchild.
356      * <p>
357      * Both files <em>must</em> have been resolved using
358      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
359      * attacks.
360      *
361      * @hide
362      */
contains(File dir, File file)363     public static boolean contains(File dir, File file) {
364         if (dir == null || file == null) return false;
365         return contains(dir.getAbsolutePath(), file.getAbsolutePath());
366     }
367 
368     /**
369      * Test if a file lives under the given directory, either as a direct child
370      * or a distant grandchild.
371      * <p>
372      * Both files <em>must</em> have been resolved using
373      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
374      * attacks.
375      *
376      * @hide
377      */
contains(String dirPath, String filePath)378     public static boolean contains(String dirPath, String filePath) {
379         if (dirPath.equals(filePath)) {
380             return true;
381         }
382         if (!dirPath.endsWith("/")) {
383             dirPath += "/";
384         }
385         return filePath.startsWith(dirPath);
386     }
387 
388     /**
389      * Write {@link String} to the given {@link File}. Deletes any existing file
390      * when the argument is {@link Optional#empty()}.
391      */
writeString(@onNull File file, @NonNull Optional<String> value)392     public static void writeString(@NonNull File file, @NonNull Optional<String> value)
393             throws IOException {
394         if (value.isPresent()) {
395             Files.write(file.toPath(), value.get().getBytes(StandardCharsets.UTF_8));
396         } else {
397             file.delete();
398         }
399     }
400 
401     private static final int MAX_READ_STRING_SIZE = 4096;
402 
403     /**
404      * Read given {@link File} as a single {@link String}. Returns
405      * {@link Optional#empty()} when
406      * <ul>
407      * <li> the file doesn't exist or
408      * <li> the size of the file exceeds {@code MAX_READ_STRING_SIZE}
409      * </ul>
410      */
readString(@onNull File file)411     public static @NonNull Optional<String> readString(@NonNull File file) throws IOException {
412         try {
413             if (file.length() <= MAX_READ_STRING_SIZE) {
414                 final String value = new String(Files.readAllBytes(file.toPath()),
415                         StandardCharsets.UTF_8);
416                 return Optional.of(value);
417             }
418             // When file size exceeds MAX_READ_STRING_SIZE, file is either
419             // corrupted or doesn't the contain expected data. Hence we return
420             // Optional.empty() which will be interpreted as empty file.
421             Logging.logPersistent(String.format("Ignored reading %s, file size exceeds %d", file,
422                     MAX_READ_STRING_SIZE));
423         } catch (NoSuchFileException ignored) {
424         }
425         return Optional.empty();
426     }
427 
428     /**
429      * Recursively walk the contents of the given {@link Path}, invoking the
430      * given {@link Consumer} for every file and directory encountered. This is
431      * typically used for recursively deleting a directory tree.
432      * <p>
433      * Gracefully attempts to process as much as possible in the face of any
434      * failures.
435      */
walkFileTreeContents(@onNull Path path, @NonNull Consumer<Path> operation)436     public static void walkFileTreeContents(@NonNull Path path, @NonNull Consumer<Path> operation) {
437         try {
438             Files.walkFileTree(path, new FileVisitor<Path>() {
439                 @Override
440                 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
441                     return FileVisitResult.CONTINUE;
442                 }
443 
444                 @Override
445                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
446                     if (!Objects.equals(path, file)) {
447                         operation.accept(file);
448                     }
449                     return FileVisitResult.CONTINUE;
450                 }
451 
452                 @Override
453                 public FileVisitResult visitFileFailed(Path file, IOException e) {
454                     Log.w(TAG, "Failed to visit " + file, e);
455                     return FileVisitResult.CONTINUE;
456                 }
457 
458                 @Override
459                 public FileVisitResult postVisitDirectory(Path dir, IOException e) {
460                     if (!Objects.equals(path, dir)) {
461                         operation.accept(dir);
462                     }
463                     return FileVisitResult.CONTINUE;
464                 }
465             });
466         } catch (IOException e) {
467             Log.w(TAG, "Failed to walk " + path, e);
468         }
469     }
470 
471     /**
472      * Recursively delete all contents inside the given directory. Gracefully
473      * attempts to delete as much as possible in the face of any failures.
474      *
475      * @deprecated if you're calling this from inside {@code MediaProvider}, you
476      *             likely want to call {@link #forEach} with a separate
477      *             invocation to invalidate FUSE entries.
478      */
479     @Deprecated
deleteContents(@onNull File dir)480     public static void deleteContents(@NonNull File dir) {
481         walkFileTreeContents(dir.toPath(), (path) -> {
482             path.toFile().delete();
483         });
484     }
485 
isValidFatFilenameChar(char c)486     private static boolean isValidFatFilenameChar(char c) {
487         if ((0x00 <= c && c <= 0x1f)) {
488             return false;
489         }
490         switch (c) {
491             case '"':
492             case '*':
493             case '/':
494             case ':':
495             case '<':
496             case '>':
497             case '?':
498             case '\\':
499             case '|':
500             case 0x7F:
501                 return false;
502             default:
503                 return true;
504         }
505     }
506 
507     /**
508      * Check if given filename is valid for a FAT filesystem.
509      *
510      * @hide
511      */
isValidFatFilename(String name)512     public static boolean isValidFatFilename(String name) {
513         return (name != null) && name.equals(buildValidFatFilename(name));
514     }
515 
516     /**
517      * Mutate the given filename to make it valid for a FAT filesystem,
518      * replacing any invalid characters with "_".
519      *
520      * @hide
521      */
buildValidFatFilename(String name)522     public static String buildValidFatFilename(String name) {
523         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
524             return "(invalid)";
525         }
526         final StringBuilder res = new StringBuilder(name.length());
527         for (int i = 0; i < name.length(); i++) {
528             final char c = name.charAt(i);
529             if (isValidFatFilenameChar(c)) {
530                 res.append(c);
531             } else {
532                 res.append('_');
533             }
534         }
535 
536         trimFilename(res, MAX_FILENAME_BYTES);
537         return res.toString();
538     }
539 
540     /** {@hide} */
541     // @VisibleForTesting
trimFilename(String str, int maxBytes)542     public static String trimFilename(String str, int maxBytes) {
543         final StringBuilder res = new StringBuilder(str);
544         trimFilename(res, maxBytes);
545         return res.toString();
546     }
547 
548     /** {@hide} */
trimFilename(StringBuilder res, int maxBytes)549     private static void trimFilename(StringBuilder res, int maxBytes) {
550         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
551         if (raw.length > maxBytes) {
552             maxBytes -= 3;
553             while (raw.length > maxBytes) {
554                 res.deleteCharAt(res.length() / 2);
555                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
556             }
557             res.insert(res.length() / 2, "...");
558         }
559     }
560 
561     /** {@hide} */
buildUniqueFileWithExtension(File parent, String name, String ext)562     private static File buildUniqueFileWithExtension(File parent, String name, String ext)
563             throws FileNotFoundException {
564         final Iterator<String> names = buildUniqueNameIterator(parent, name);
565         while (names.hasNext()) {
566             File file = buildFile(parent, names.next(), ext);
567             if (!file.exists()) {
568                 return file;
569             }
570         }
571         throw new FileNotFoundException("Failed to create unique file");
572     }
573 
574     private static final Pattern PATTERN_DCF_STRICT = Pattern
575             .compile("([A-Z0-9_]{4})([0-9]{4})");
576     private static final Pattern PATTERN_DCF_RELAXED = Pattern
577             .compile("((?:IMG|MVIMG|VID)_[0-9]{8}_[0-9]{6})(?:~([0-9]+))?");
578 
isDcim(@onNull File dir)579     private static boolean isDcim(@NonNull File dir) {
580         while (dir != null) {
581             if (Objects.equals("DCIM", dir.getName())) {
582                 return true;
583             }
584             dir = dir.getParentFile();
585         }
586         return false;
587     }
588 
buildUniqueNameIterator(@onNull File parent, @NonNull String name)589     private static @NonNull Iterator<String> buildUniqueNameIterator(@NonNull File parent,
590             @NonNull String name) {
591         if (isDcim(parent)) {
592             final Matcher dcfStrict = PATTERN_DCF_STRICT.matcher(name);
593             if (dcfStrict.matches()) {
594                 // Generate names like "IMG_1001"
595                 final String prefix = dcfStrict.group(1);
596                 return new Iterator<String>() {
597                     int i = Integer.parseInt(dcfStrict.group(2));
598                     @Override
599                     public String next() {
600                         final String res = String.format(Locale.US, "%s%04d", prefix, i);
601                         i++;
602                         return res;
603                     }
604                     @Override
605                     public boolean hasNext() {
606                         return i <= 9999;
607                     }
608                 };
609             }
610 
611             final Matcher dcfRelaxed = PATTERN_DCF_RELAXED.matcher(name);
612             if (dcfRelaxed.matches()) {
613                 // Generate names like "IMG_20190102_030405~2"
614                 final String prefix = dcfRelaxed.group(1);
615                 return new Iterator<String>() {
616                     int i = TextUtils.isEmpty(dcfRelaxed.group(2))
617                             ? 1
618                             : Integer.parseInt(dcfRelaxed.group(2));
619                     @Override
620                     public String next() {
621                         final String res = (i == 1)
622                             ? prefix
623                             : String.format(Locale.US, "%s~%d", prefix, i);
624                         i++;
625                         return res;
626                     }
627                     @Override
628                     public boolean hasNext() {
629                         return i <= 99;
630                     }
631                 };
632             }
633         }
634 
635         // Generate names like "foo (2)"
636         return new Iterator<String>() {
637             int i = 0;
638             @Override
639             public String next() {
640                 final String res = (i == 0) ? name : name + " (" + i + ")";
641                 i++;
642                 return res;
643             }
644             @Override
645             public boolean hasNext() {
646                 return i < 32;
647             }
648         };
649     }
650 
651     /**
652      * Generates a unique file name under the given parent directory. If the display name doesn't
653      * have an extension that matches the requested MIME type, the default extension for that MIME
654      * type is appended. If a file already exists, the name is appended with a numerical value to
655      * make it unique.
656      *
657      * For example, the display name 'example' with 'text/plain' MIME might produce
658      * 'example.txt' or 'example (1).txt', etc.
659      *
660      * @throws FileNotFoundException
661      * @hide
662      */
663     public static File buildUniqueFile(File parent, String mimeType, String displayName)
664             throws FileNotFoundException {
665         final String[] parts = splitFileName(mimeType, displayName);
666         return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
667     }
668 
669     /** {@hide} */
670     public static File buildNonUniqueFile(File parent, String mimeType, String displayName) {
671         final String[] parts = splitFileName(mimeType, displayName);
672         return buildFile(parent, parts[0], parts[1]);
673     }
674 
675     /**
676      * Generates a unique file name under the given parent directory, keeping
677      * any extension intact.
678      *
679      * @hide
680      */
681     public static File buildUniqueFile(File parent, String displayName)
682             throws FileNotFoundException {
683         final String name;
684         final String ext;
685 
686         // Extract requested extension from display name
687         final int lastDot = displayName.lastIndexOf('.');
688         if (lastDot >= 0) {
689             name = displayName.substring(0, lastDot);
690             ext = displayName.substring(lastDot + 1);
691         } else {
692             name = displayName;
693             ext = null;
694         }
695 
696         return buildUniqueFileWithExtension(parent, name, ext);
697     }
698 
699     /**
700      * Splits file name into base name and extension.
701      * If the display name doesn't have an extension that matches the requested MIME type, the
702      * extension is regarded as a part of filename and default extension for that MIME type is
703      * appended.
704      *
705      * @hide
706      */
707     public static String[] splitFileName(String mimeType, String displayName) {
708         String name;
709         String ext;
710 
711         {
712             String mimeTypeFromExt;
713 
714             // Extract requested extension from display name
715             final int lastDot = displayName.lastIndexOf('.');
716             if (lastDot > 0) {
717                 name = displayName.substring(0, lastDot);
718                 ext = displayName.substring(lastDot + 1);
719                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
720                         ext.toLowerCase(Locale.ROOT));
721             } else {
722                 name = displayName;
723                 ext = null;
724                 mimeTypeFromExt = null;
725             }
726 
727             if (mimeTypeFromExt == null) {
728                 mimeTypeFromExt = ClipDescription.MIMETYPE_UNKNOWN;
729             }
730 
731             final String extFromMimeType;
732             if (ClipDescription.MIMETYPE_UNKNOWN.equalsIgnoreCase(mimeType)) {
733                 extFromMimeType = null;
734             } else {
735                 extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
736             }
737 
738             if (MimeUtils.equalIgnoreCase(mimeType, mimeTypeFromExt)
739                     || MimeUtils.equalIgnoreCase(ext, extFromMimeType)) {
740                 // Extension maps back to requested MIME type; allow it
741             } else {
742                 // No match; insist that create file matches requested MIME
743                 name = displayName;
744                 ext = extFromMimeType;
745             }
746         }
747 
748         if (ext == null) {
749             ext = "";
750         }
751 
752         return new String[] { name, ext };
753     }
754 
755     /** {@hide} */
756     private static File buildFile(File parent, String name, String ext) {
757         if (TextUtils.isEmpty(ext)) {
758             return new File(parent, name);
759         } else {
760             return new File(parent, name + "." + ext);
761         }
762     }
763 
764     public static @Nullable String extractDisplayName(@Nullable String data) {
765         if (data == null) return null;
766         if (data.indexOf('/') == -1) {
767             return data;
768         }
769         if (data.endsWith("/")) {
770             data = data.substring(0, data.length() - 1);
771         }
772         return data.substring(data.lastIndexOf('/') + 1);
773     }
774 
775     public static @Nullable String extractFileName(@Nullable String data) {
776         if (data == null) return null;
777         data = extractDisplayName(data);
778 
779         final int lastDot = data.lastIndexOf('.');
780         if (lastDot == -1) {
781             return data;
782         } else {
783             return data.substring(0, lastDot);
784         }
785     }
786 
787     public static @Nullable String extractFileExtension(@Nullable String data) {
788         if (data == null) return null;
789         data = extractDisplayName(data);
790 
791         final int lastDot = data.lastIndexOf('.');
792         if (lastDot == -1) {
793             return null;
794         } else {
795             return data.substring(lastDot + 1);
796         }
797     }
798 
799     /**
800      * Return list of paths that should be scanned with
801      * {@link com.android.providers.media.scan.MediaScanner} for the given
802      * volume name.
803      */
804     public static @NonNull Collection<File> getVolumeScanPaths(@NonNull Context context,
805             @NonNull String volumeName) throws FileNotFoundException {
806         final ArrayList<File> res = new ArrayList<>();
807         switch (volumeName) {
808             case MediaStore.VOLUME_INTERNAL: {
809                 res.addAll(Environment.getInternalMediaDirectories());
810                 break;
811             }
812             case MediaStore.VOLUME_EXTERNAL: {
813                 for (String resolvedVolumeName : MediaStore.getExternalVolumeNames(context)) {
814                     res.add(getVolumePath(context, resolvedVolumeName));
815                 }
816                 break;
817             }
818             default: {
819                 res.add(getVolumePath(context, volumeName));
820             }
821         }
822         return res;
823     }
824 
825     /**
826      * Return path where the given volume name is mounted.
827      */
828     public static @NonNull File getVolumePath(@NonNull Context context,
829             @NonNull String volumeName) throws FileNotFoundException {
830         switch (volumeName) {
831             case MediaStore.VOLUME_INTERNAL:
832             case MediaStore.VOLUME_EXTERNAL:
833                 throw new FileNotFoundException(volumeName + " has no associated path");
834         }
835 
836         final Uri uri = MediaStore.Files.getContentUri(volumeName);
837         File path = null;
838 
839         try {
840             path = context.getSystemService(StorageManager.class).getStorageVolume(uri)
841                     .getDirectory();
842         } catch (IllegalStateException e) {
843             Log.w("Ignoring volume not found exception", e);
844         }
845 
846         if (path != null) {
847             return path;
848         } else {
849             throw new FileNotFoundException(volumeName + " has no associated path");
850         }
851     }
852 
853     /**
854      * Returns the content URI for the volume that contains the given path.
855      *
856      * <p>{@link MediaStore.Files#getContentUriForPath(String)} can't detect public volumes and can
857      * only return the URI for the primary external storage, that's why this utility should be used
858      * instead.
859      */
860     public static @NonNull Uri getContentUriForPath(@NonNull String path) {
861         Objects.requireNonNull(path);
862         return MediaStore.Files.getContentUri(extractVolumeName(path));
863     }
864 
865     /**
866      * Return StorageVolume corresponding to the file on Path
867      */
868     public static @NonNull StorageVolume getStorageVolume(@NonNull Context context,
869             @NonNull File path) throws FileNotFoundException {
870         int userId = extractUserId(path.getPath());
871         Context userContext = context;
872         if (userId >= 0 && (context.getUser().getIdentifier() != userId)) {
873             // This volume is for a different user than our context, create a context
874             // for that user to retrieve the correct volume.
875             try {
876                 userContext = context.createPackageContextAsUser("system", 0,
877                         UserHandle.of(userId));
878             } catch (PackageManager.NameNotFoundException e) {
879                 throw new FileNotFoundException("Can't get package context for user " + userId);
880             }
881         }
882 
883         StorageVolume volume = userContext.getSystemService(StorageManager.class)
884                 .getStorageVolume(path);
885         if (volume == null) {
886             throw new FileNotFoundException("Can't find volume for " + path.getPath());
887         }
888 
889         return volume;
890     }
891 
892     /**
893      * Return volume name which hosts the given path.
894      */
895     public static @NonNull String getVolumeName(@NonNull Context context, @NonNull File path)
896             throws FileNotFoundException {
897         if (contains(Environment.getStorageDirectory(), path)) {
898             StorageVolume volume = getStorageVolume(context, path);
899             return volume.getMediaStoreVolumeName();
900         } else {
901             return MediaStore.VOLUME_INTERNAL;
902         }
903     }
904 
905     public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile(
906             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/.+");
907     public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile(
908             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/?");
909     public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
910             "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$");
911     public static final Pattern PATTERN_PENDING_FILEPATH_FOR_SQL = Pattern.compile(
912             ".*/\\.pending-(\\d+)-([^/]+)$");
913 
914     /**
915      * File prefix indicating that the file {@link MediaColumns#IS_PENDING}.
916      */
917     public static final String PREFIX_PENDING = "pending";
918 
919     /**
920      * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}.
921      */
922     public static final String PREFIX_TRASHED = "trashed";
923 
924     /**
925      * Default duration that {@link MediaColumns#IS_PENDING} items should be
926      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
927      */
928     public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS;
929 
930     /**
931      * Default duration that {@link MediaColumns#IS_TRASHED} items should be
932      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
933      */
934     public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS;
935 
936     /**
937      * Default duration that expired items should be extended in
938      * {@link #runIdleMaintenance}.
939      */
940     public static final long DEFAULT_DURATION_EXTENDED = 7 * DateUtils.DAY_IN_MILLIS;
941 
942     public static boolean isDownload(@NonNull String path) {
943         return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
944     }
945 
946     public static boolean isDownloadDir(@NonNull String path) {
947         return PATTERN_DOWNLOADS_DIRECTORY.matcher(path).matches();
948     }
949 
950     private static final boolean PROP_CROSS_USER_ALLOWED =
951             SystemProperties.getBoolean("external_storage.cross_user.enabled", false);
952 
953     private static final String PROP_CROSS_USER_ROOT = isCrossUserEnabled()
954             ? SystemProperties.get("external_storage.cross_user.root", "") : "";
955 
956     private static final String PROP_CROSS_USER_ROOT_PATTERN = ((PROP_CROSS_USER_ROOT.isEmpty())
957             ? "" : "(?:" + PROP_CROSS_USER_ROOT + "/)?");
958 
959     /**
960      * Regex that matches paths in all well-known package-specific directories,
961      * and which captures the package name as the first group.
962      */
963     public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
964             "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
965             + PROP_CROSS_USER_ROOT_PATTERN
966             + "Android/(?:data|media|obb)/([^/]+)(/?.*)?");
967 
968     /**
969      * Regex that matches Android/obb or Android/data path.
970      */
971     public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
972             "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
973             + PROP_CROSS_USER_ROOT_PATTERN
974             + "Android/(?:data|obb)/?$");
975 
976     /**
977      * Regex that matches Android/obb paths.
978      */
979     public static final Pattern PATTERN_OBB_OR_CHILD_PATH = Pattern.compile(
980             "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
981             + PROP_CROSS_USER_ROOT_PATTERN
982             + "Android/(?:obb)(/?.*)");
983 
984     /**
985      * The recordings directory. This is used for R OS. For S OS or later,
986      * we use {@link Environment#DIRECTORY_RECORDINGS} directly.
987      */
988     public static final String DIRECTORY_RECORDINGS = "Recordings";
989 
990     @VisibleForTesting
991     public static final String[] DEFAULT_FOLDER_NAMES;
992     static {
993         if (SdkLevel.isAtLeastS()) {
994             DEFAULT_FOLDER_NAMES = new String[]{
995                     Environment.DIRECTORY_MUSIC,
996                     Environment.DIRECTORY_PODCASTS,
997                     Environment.DIRECTORY_RINGTONES,
998                     Environment.DIRECTORY_ALARMS,
999                     Environment.DIRECTORY_NOTIFICATIONS,
1000                     Environment.DIRECTORY_PICTURES,
1001                     Environment.DIRECTORY_MOVIES,
1002                     Environment.DIRECTORY_DOWNLOADS,
1003                     Environment.DIRECTORY_DCIM,
1004                     Environment.DIRECTORY_DOCUMENTS,
1005                     Environment.DIRECTORY_AUDIOBOOKS,
1006                     Environment.DIRECTORY_RECORDINGS,
1007             };
1008         } else {
1009             DEFAULT_FOLDER_NAMES = new String[]{
1010                     Environment.DIRECTORY_MUSIC,
1011                     Environment.DIRECTORY_PODCASTS,
1012                     Environment.DIRECTORY_RINGTONES,
1013                     Environment.DIRECTORY_ALARMS,
1014                     Environment.DIRECTORY_NOTIFICATIONS,
1015                     Environment.DIRECTORY_PICTURES,
1016                     Environment.DIRECTORY_MOVIES,
1017                     Environment.DIRECTORY_DOWNLOADS,
1018                     Environment.DIRECTORY_DCIM,
1019                     Environment.DIRECTORY_DOCUMENTS,
1020                     Environment.DIRECTORY_AUDIOBOOKS,
1021                     DIRECTORY_RECORDINGS,
1022             };
1023         }
1024     }
1025 
1026     /**
1027      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}
1028      */
1029     private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
1030             "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)");
1031 
1032     /**
1033      * Regex that matches paths under well-known storage paths.
1034      */
1035     private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
1036             "(?i)^/storage/([^/]+)");
1037 
1038     /**
1039      * Regex that matches user-ids under well-known storage paths.
1040      */
1041     private static final Pattern PATTERN_USER_ID = Pattern.compile(
1042             "(?i)^/storage/emulated/([0-9]+)");
1043 
1044     private static final String CAMERA_RELATIVE_PATH =
1045             String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera");
1046 
1047     public static boolean isCrossUserEnabled() {
1048         return PROP_CROSS_USER_ALLOWED || SdkLevel.isAtLeastS();
1049     }
1050 
1051     private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
1052         return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
1053     }
1054 
1055     public static int extractUserId(@Nullable String data) {
1056         if (data == null) return -1;
1057         final Matcher matcher = PATTERN_USER_ID.matcher(data);
1058         if (matcher.find()) {
1059             return Integer.parseInt(matcher.group(1));
1060         }
1061 
1062         return -1;
1063     }
1064 
1065     public static @Nullable String extractVolumePath(@Nullable String data) {
1066         if (data == null) return null;
1067         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
1068         if (matcher.find()) {
1069             return data.substring(0, matcher.end());
1070         } else {
1071             return null;
1072         }
1073     }
1074 
1075     public static @Nullable String extractVolumeName(@Nullable String data) {
1076         if (data == null) return null;
1077         final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
1078         if (matcher.find()) {
1079             final String volumeName = matcher.group(1);
1080             if (volumeName.equals("emulated")) {
1081                 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
1082             } else {
1083                 return normalizeUuid(volumeName);
1084             }
1085         } else {
1086             return MediaStore.VOLUME_INTERNAL;
1087         }
1088     }
1089 
1090     public static @Nullable String extractRelativePath(@Nullable String data) {
1091         if (data == null) return null;
1092         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
1093         if (matcher.find()) {
1094             final int lastSlash = data.lastIndexOf('/');
1095             if (lastSlash == -1 || lastSlash < matcher.end()) {
1096                 // This is a file in the top-level directory, so relative path is "/"
1097                 // which is different than null, which means unknown path
1098                 return "/";
1099             } else {
1100                 return data.substring(matcher.end(), lastSlash + 1);
1101             }
1102         } else {
1103             return null;
1104         }
1105     }
1106 
1107     /**
1108      * Returns relative path for the directory.
1109      */
1110     @VisibleForTesting
1111     public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
1112         if (directoryPath == null) return null;
1113 
1114         if (directoryPath.equals("/storage/emulated") ||
1115                 directoryPath.equals("/storage/emulated/")) {
1116             // This path is not reachable for MediaProvider.
1117             return null;
1118         }
1119 
1120         // We are extracting relative path for the directory itself, we add "/" so that we can use
1121         // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
1122         // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
1123         // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
1124         if (!directoryPath.endsWith("/")) {
1125             // Relative path for directory should end with "/".
1126             directoryPath += "/";
1127         }
1128 
1129         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
1130         if (matcher.find()) {
1131             if (matcher.end() == directoryPath.length()) {
1132                 // This is the top-level directory, so relative path is "/"
1133                 return "/";
1134             }
1135             return directoryPath.substring(matcher.end());
1136         }
1137         return null;
1138     }
1139 
1140     public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
1141         if (path == null) return null;
1142         final Matcher m = PATTERN_OWNED_PATH.matcher(path);
1143         if (m.matches()) {
1144             return m.group(1);
1145         } else {
1146             return null;
1147         }
1148     }
1149 
1150     public static boolean isExternalMediaDirectory(@NonNull String path) {
1151         return isExternalMediaDirectory(path, PROP_CROSS_USER_ROOT);
1152     }
1153 
1154     @VisibleForTesting
1155     static boolean isExternalMediaDirectory(@NonNull String path, String crossUserRoot) {
1156         final String relativePath = extractRelativePath(path);
1157         if (relativePath != null) {
1158             final String externalMediaDir = (crossUserRoot == null || crossUserRoot.isEmpty())
1159                     ? "Android/media" : crossUserRoot + "/Android/media";
1160             return relativePath.startsWith(externalMediaDir);
1161         }
1162         return false;
1163     }
1164 
1165     /**
1166      * Returns true if relative path is Android/data or Android/obb path.
1167      */
1168     public static boolean isDataOrObbPath(String path) {
1169         if (path == null) return false;
1170         final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
1171         return m.matches();
1172     }
1173 
1174     /**
1175      * Returns true if relative path is Android/obb path.
1176      */
1177     public static boolean isObbOrChildPath(String path) {
1178         if (path == null) return false;
1179         final Matcher m = PATTERN_OBB_OR_CHILD_PATH.matcher(path);
1180         return m.matches();
1181     }
1182 
1183     /**
1184      * Returns the name of the top level directory, or null if the path doesn't go through the
1185      * external storage directory.
1186      */
1187     @Nullable
1188     public static String extractTopLevelDir(String path) {
1189         final String relativePath = extractRelativePath(path);
1190         if (relativePath == null) {
1191             return null;
1192         }
1193 
1194         return extractTopLevelDir(relativePath.split("/"));
1195     }
1196 
1197     @Nullable
1198     public static String extractTopLevelDir(String[] relativePathSegments) {
1199         return extractTopLevelDir(relativePathSegments, PROP_CROSS_USER_ROOT);
1200     }
1201 
1202     @VisibleForTesting
1203     @Nullable
1204     static String extractTopLevelDir(String[] relativePathSegments, String crossUserRoot) {
1205         if (relativePathSegments == null) return null;
1206 
1207         final String topLevelDir = relativePathSegments.length > 0 ? relativePathSegments[0] : null;
1208         if (crossUserRoot != null && crossUserRoot.equals(topLevelDir)) {
1209             return relativePathSegments.length > 1 ? relativePathSegments[1] : null;
1210         }
1211 
1212         return topLevelDir;
1213     }
1214 
1215     public static boolean isDefaultDirectoryName(@Nullable String dirName) {
1216         for (String defaultDirName : DEFAULT_FOLDER_NAMES) {
1217             if (defaultDirName.equalsIgnoreCase(dirName)) {
1218                 return true;
1219             }
1220         }
1221         return false;
1222     }
1223 
1224     /**
1225      * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
1226      * columns being modified by this operation.
1227      */
1228     public static void computeDateExpires(@NonNull ContentValues values) {
1229         // External apps have no ability to change this field
1230         values.remove(MediaColumns.DATE_EXPIRES);
1231 
1232         // Only define the field when this modification is actually adjusting
1233         // one of the flags that should influence the expiration
1234         final Object pending = values.get(MediaColumns.IS_PENDING);
1235         if (pending != null) {
1236             if (parseBoolean(pending, false)) {
1237                 values.put(MediaColumns.DATE_EXPIRES,
1238                         (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1239             } else {
1240                 values.putNull(MediaColumns.DATE_EXPIRES);
1241             }
1242         }
1243         final Object trashed = values.get(MediaColumns.IS_TRASHED);
1244         if (trashed != null) {
1245             if (parseBoolean(trashed, false)) {
1246                 values.put(MediaColumns.DATE_EXPIRES,
1247                         (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1248             } else {
1249                 values.putNull(MediaColumns.DATE_EXPIRES);
1250             }
1251         }
1252     }
1253 
1254     /**
1255      * Compute several scattered {@link MediaColumns} values from
1256      * {@link MediaColumns#DATA}. This method performs no enforcement of
1257      * argument validity.
1258      */
1259     public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) {
1260         // Worst case we have to assume no bucket details
1261         values.remove(MediaColumns.VOLUME_NAME);
1262         values.remove(MediaColumns.RELATIVE_PATH);
1263         values.remove(MediaColumns.IS_TRASHED);
1264         values.remove(MediaColumns.DATE_EXPIRES);
1265         values.remove(MediaColumns.DISPLAY_NAME);
1266         values.remove(MediaColumns.BUCKET_ID);
1267         values.remove(MediaColumns.BUCKET_DISPLAY_NAME);
1268 
1269         final String data = values.getAsString(MediaColumns.DATA);
1270         if (TextUtils.isEmpty(data)) return;
1271 
1272         final File file = new File(data);
1273         final File fileLower = new File(data.toLowerCase(Locale.ROOT));
1274 
1275         values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
1276         values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
1277         final String displayName = extractDisplayName(data);
1278         final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
1279         if (matcher.matches()) {
1280             values.put(MediaColumns.IS_PENDING,
1281                     matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0);
1282             values.put(MediaColumns.IS_TRASHED,
1283                     matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0);
1284             values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2)));
1285             values.put(MediaColumns.DISPLAY_NAME, matcher.group(3));
1286         } else {
1287             if (isForFuse) {
1288                 // Allow Fuse thread to set IS_PENDING when using DATA column.
1289                 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify
1290                 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't
1291                 // explicitly specify the value of IS_PENDING.
1292             } else {
1293                 values.put(MediaColumns.IS_PENDING, 0);
1294             }
1295             values.put(MediaColumns.IS_TRASHED, 0);
1296             values.putNull(MediaColumns.DATE_EXPIRES);
1297             values.put(MediaColumns.DISPLAY_NAME, displayName);
1298         }
1299 
1300         // Buckets are the parent directory
1301         final String parent = fileLower.getParent();
1302         if (parent != null) {
1303             values.put(MediaColumns.BUCKET_ID, parent.hashCode());
1304             // The relative path for files in the top directory is "/"
1305             if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) {
1306                 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
1307             }
1308         }
1309     }
1310 
1311     /**
1312      * Compute {@link MediaColumns#DATA} from several scattered
1313      * {@link MediaColumns} values.  This method performs no enforcement of
1314      * argument validity.
1315      */
1316     public static void computeDataFromValues(@NonNull ContentValues values,
1317             @NonNull File volumePath, boolean isForFuse) {
1318         values.remove(MediaColumns.DATA);
1319 
1320         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1321         final String resolvedDisplayName;
1322         // Pending file path shouldn't be rewritten for files inserted via filepath.
1323         if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
1324             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1325                     (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1326             final String combinedString = String.format(
1327                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName);
1328             // trim the file name to avoid ENAMETOOLONG error
1329             // after trim the file, if the user unpending the file,
1330             // the file name is not the original one
1331             resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
1332         } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
1333             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1334                     (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1335             final String combinedString = String.format(
1336                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName);
1337             // trim the file name to avoid ENAMETOOLONG error
1338             // after trim the file, if the user untrashes the file,
1339             // the file name is not the original one
1340             resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
1341         } else {
1342             resolvedDisplayName = displayName;
1343         }
1344 
1345         final File filePath = buildPath(volumePath,
1346                 values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName);
1347         values.put(MediaColumns.DATA, filePath.getAbsolutePath());
1348     }
1349 
1350     public static void sanitizeValues(@NonNull ContentValues values,
1351             boolean rewriteHiddenFileName) {
1352         final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
1353         for (int i = 0; i < relativePath.length; i++) {
1354             relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName);
1355         }
1356         values.put(MediaColumns.RELATIVE_PATH,
1357                 String.join("/", relativePath) + "/");
1358 
1359         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1360         values.put(MediaColumns.DISPLAY_NAME,
1361                 sanitizeDisplayName(displayName, rewriteHiddenFileName));
1362     }
1363 
1364     /** {@hide} **/
1365     @Nullable
1366     public static String getAbsoluteSanitizedPath(String path) {
1367         final String[] pathSegments = sanitizePath(path);
1368         if (pathSegments.length == 0) {
1369             return null;
1370         }
1371         return path = "/" + String.join("/",
1372                 Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
1373     }
1374 
1375     /** {@hide} */
1376     public static @NonNull String[] sanitizePath(@Nullable String path) {
1377         if (path == null) {
1378             return new String[0];
1379         } else {
1380             final String[] segments = path.split("/");
1381             // If the path corresponds to the top level directory, then we return an empty path
1382             // which denotes the top level directory
1383             if (segments.length == 0) {
1384                 return new String[] { "" };
1385             }
1386             for (int i = 0; i < segments.length; i++) {
1387                 segments[i] = sanitizeDisplayName(segments[i]);
1388             }
1389             return segments;
1390         }
1391     }
1392 
1393     /**
1394      * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem.
1395      * @hide
1396      */
1397     public static @Nullable String sanitizeDisplayName(@Nullable String name) {
1398         return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false);
1399     }
1400 
1401     /**
1402      * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to
1403      * make it valid for a FAT filesystem.
1404      * @hide
1405      */
1406     public static @Nullable String sanitizeDisplayName(@Nullable String name,
1407             boolean rewriteHiddenFileName) {
1408         if (name == null) {
1409             return null;
1410         } else if (rewriteHiddenFileName && name.startsWith(".")) {
1411             // The resulting file must not be hidden.
1412             return "_" + name;
1413         } else {
1414             return buildValidFatFilename(name);
1415         }
1416     }
1417 
1418     /**
1419      * Test if this given directory should be considered hidden.
1420      */
1421     @VisibleForTesting
1422     public static boolean isDirectoryHidden(@NonNull File dir) {
1423         final String name = dir.getName();
1424         if (name.startsWith(".")) {
1425             return true;
1426         }
1427 
1428         final File nomedia = new File(dir, ".nomedia");
1429 
1430         // check for .nomedia presence
1431         if (!nomedia.exists()) {
1432             return false;
1433         }
1434 
1435         // Handle top-level default directories. These directories should always be visible,
1436         // regardless of .nomedia presence.
1437         final String[] relativePath = sanitizePath(extractRelativePath(dir.getAbsolutePath()));
1438         final boolean isTopLevelDir =
1439                 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
1440         if (isTopLevelDir && isDefaultDirectoryName(name)) {
1441             nomedia.delete();
1442             return false;
1443         }
1444 
1445         // DCIM/Camera should always be visible regardless of .nomedia presence.
1446         if (CAMERA_RELATIVE_PATH.equalsIgnoreCase(
1447                 extractRelativePathForDirectory(dir.getAbsolutePath()))) {
1448             nomedia.delete();
1449             return false;
1450         }
1451 
1452         if (isScreenshotsDirNonHidden(relativePath, name)) {
1453             nomedia.delete();
1454             return false;
1455         }
1456 
1457         // .nomedia is present which makes this directory as hidden directory
1458         Logging.logPersistent("Observed non-standard " + nomedia);
1459         return true;
1460     }
1461 
1462     /**
1463      * Consider Screenshots directory in root directory or inside well-known directory as always
1464      * non-hidden. Nomedia file in these directories will not be able to hide these directories.
1465      * i.e., some examples of directories that will be considered non-hidden are
1466      * <ul>
1467      * <li> /storage/emulated/0/Screenshots or
1468      * <li> /storage/emulated/0/DCIM/Screenshots or
1469      * <li> /storage/emulated/0/Pictures/Screenshots ...
1470      * </ul>
1471      * Some examples of directories that can be considered as hidden with nomedia are
1472      * <ul>
1473      * <li> /storage/emulated/0/foo/Screenshots or
1474      * <li> /storage/emulated/0/DCIM/Foo/Screenshots or
1475      * <li> /storage/emulated/0/Pictures/foo/bar/Screenshots ...
1476      * </ul>
1477      */
1478     private static boolean isScreenshotsDirNonHidden(@NonNull String[] relativePath,
1479             @NonNull String name) {
1480         if (name.equalsIgnoreCase(Environment.DIRECTORY_SCREENSHOTS)) {
1481             return (relativePath.length == 1 &&
1482                 (TextUtils.isEmpty(relativePath[0]) || isDefaultDirectoryName(relativePath[0])));
1483         }
1484         return false;
1485     }
1486 
1487     /**
1488      * Test if this given file should be considered hidden.
1489      */
1490     @VisibleForTesting
1491     public static boolean isFileHidden(@NonNull File file) {
1492         final String name = file.getName();
1493 
1494         // Handle well-known file names that are pending or trashed; they
1495         // normally appear hidden, but we give them special treatment
1496         if (PATTERN_EXPIRES_FILE.matcher(name).matches()) {
1497             return false;
1498         }
1499 
1500         // Otherwise fall back to file name
1501         if (name.startsWith(".")) {
1502             return true;
1503         }
1504         return false;
1505     }
1506 
1507     /**
1508      * Clears all app's external cache directories, i.e. for each app we delete
1509      * /sdcard/Android/data/app/cache/* but we keep the directory itself.
1510      *
1511      * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs.
1512      *
1513      * <p>This method doesn't perform any checks, so make sure that the calling package is allowed
1514      * to clear cache directories first.
1515      *
1516      * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none
1517      * or part of the directories were cleared.
1518      */
1519     public static int clearAppCacheDirectories() {
1520         int status = 0;
1521         Log.i(TAG, "Clearing cache for all apps");
1522         final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(),
1523                 "Android", "data");
1524         for (File appDataDir : rootDataDir.listFiles()) {
1525             try {
1526                 final File appCacheDir = new File(appDataDir, "cache");
1527                 if (appCacheDir.isDirectory()) {
1528                     FileUtils.deleteContents(appCacheDir);
1529                 }
1530             } catch (Exception e) {
1531                 // We want to avoid crashing MediaProvider at all costs, so we handle all "generic"
1532                 // exceptions here, and just report to the caller that an IO exception has occurred.
1533                 // We still try to clear the rest of the directories.
1534                 Log.e(TAG, "Couldn't delete all app cache dirs!", e);
1535                 status = OsConstants.EIO;
1536             }
1537         }
1538         return status;
1539     }
1540 
1541     /**
1542      * @return {@code true} if {@code dir} is dirty and should be scanned, {@code false} otherwise.
1543      */
1544     public static boolean isDirectoryDirty(File dir) {
1545         File nomedia = new File(dir, ".nomedia");
1546         if (nomedia.exists()) {
1547             try {
1548                 Optional<String> expectedPath = readString(nomedia);
1549                 // Returns true If .nomedia file is empty or content doesn't match |dir|
1550                 // Returns false otherwise
1551                 return !expectedPath.isPresent()
1552                         || !expectedPath.get().equals(dir.getPath());
1553             } catch (IOException e) {
1554                 Log.w(TAG, "Failed to read directory dirty" + dir);
1555             }
1556         }
1557         return true;
1558     }
1559 
1560     /**
1561      * {@code isDirty} == {@code true} will force {@code dir} scanning even if it's hidden
1562      * {@code isDirty} == {@code false} will skip {@code dir} scanning on next scan.
1563      */
1564     public static void setDirectoryDirty(File dir, boolean isDirty) {
1565         File nomedia = new File(dir, ".nomedia");
1566         if (nomedia.exists()) {
1567             try {
1568                 writeString(nomedia, isDirty ? Optional.of("") : Optional.of(dir.getPath()));
1569             } catch (IOException e) {
1570                 Log.w(TAG, "Failed to change directory dirty: " + dir + ". isDirty: " + isDirty);
1571             }
1572         }
1573     }
1574 
1575     /**
1576      * @return the folder containing the top-most .nomedia in {@code file} hierarchy.
1577      * E.g input as /sdcard/foo/bar/ will return /sdcard/foo
1578      * even if foo and bar contain .nomedia files.
1579      *
1580      * Returns {@code null} if there's no .nomedia in hierarchy
1581      */
1582     public static File getTopLevelNoMedia(@NonNull File file) {
1583         File topNoMediaDir = null;
1584 
1585         File parent = file;
1586         while (parent != null) {
1587             File nomedia = new File(parent, ".nomedia");
1588             if (nomedia.exists()) {
1589                 topNoMediaDir = parent;
1590             }
1591             parent = parent.getParentFile();
1592         }
1593 
1594         return topNoMediaDir;
1595     }
1596 
1597     /**
1598      * Generate the extended absolute path from the expired file path
1599      * E.g. the input expiredFilePath is /storage/emulated/0/DCIM/.trashed-1621147340-test.jpg
1600      * The returned result is /storage/emulated/0/DCIM/.trashed-1888888888-test.jpg
1601      *
1602      * @hide
1603      */
1604     @Nullable
1605     public static String getAbsoluteExtendedPath(@NonNull String expiredFilePath,
1606             long extendedTime) {
1607         final String displayName = extractDisplayName(expiredFilePath);
1608 
1609         final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(displayName);
1610         if (matcher.matches()) {
1611             final String newDisplayName = String.format(Locale.US, ".%s-%d-%s", matcher.group(1),
1612                     extendedTime, matcher.group(3));
1613             final int lastSlash = expiredFilePath.lastIndexOf('/');
1614             final String newPath = expiredFilePath.substring(0, lastSlash + 1).concat(
1615                     newDisplayName);
1616             return newPath;
1617         }
1618 
1619         return null;
1620     }
1621 }
1622