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.net.Uri;
49 import android.os.Environment;
50 import android.os.ParcelFileDescriptor;
51 import android.os.storage.StorageManager;
52 import android.provider.MediaStore;
53 import android.provider.MediaStore.MediaColumns;
54 import android.system.ErrnoException;
55 import android.system.Os;
56 import android.system.OsConstants;
57 import android.text.TextUtils;
58 import android.text.format.DateUtils;
59 import android.util.Log;
60 import android.webkit.MimeTypeMap;
61 
62 import androidx.annotation.NonNull;
63 import androidx.annotation.Nullable;
64 import androidx.annotation.VisibleForTesting;
65 
66 import java.io.File;
67 import java.io.FileDescriptor;
68 import java.io.FileNotFoundException;
69 import java.io.IOException;
70 import java.io.InputStream;
71 import java.io.OutputStream;
72 import java.nio.charset.StandardCharsets;
73 import java.nio.file.FileVisitResult;
74 import java.nio.file.FileVisitor;
75 import java.nio.file.Files;
76 import java.nio.file.NoSuchFileException;
77 import java.nio.file.Path;
78 import java.nio.file.attribute.BasicFileAttributes;
79 import java.util.ArrayList;
80 import java.util.Arrays;
81 import java.util.Collection;
82 import java.util.Comparator;
83 import java.util.Iterator;
84 import java.util.Locale;
85 import java.util.Objects;
86 import java.util.Optional;
87 import java.util.function.Consumer;
88 import java.util.regex.Matcher;
89 import java.util.regex.Pattern;
90 
91 public class FileUtils {
92     /**
93      * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)}
94      * which adds security features like {@link OsConstants#O_CLOEXEC} and
95      * {@link OsConstants#O_NOFOLLOW}.
96      */
openSafely(@onNull File file, int pfdFlags)97     public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags)
98             throws FileNotFoundException {
99         final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW;
100         try {
101             final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags,
102                     S_IRWXU | S_IRWXG);
103             try {
104                 return ParcelFileDescriptor.dup(fd);
105             } finally {
106                 closeQuietly(fd);
107             }
108         } catch (IOException | ErrnoException e) {
109             throw new FileNotFoundException(e.getMessage());
110         }
111     }
112 
closeQuietly(@ullable AutoCloseable closeable)113     public static void closeQuietly(@Nullable AutoCloseable closeable) {
114         android.os.FileUtils.closeQuietly(closeable);
115     }
116 
closeQuietly(@ullable FileDescriptor fd)117     public static void closeQuietly(@Nullable FileDescriptor fd) {
118         if (fd == null) return;
119         try {
120             Os.close(fd);
121         } catch (ErrnoException ignored) {
122         }
123     }
124 
copy(@onNull InputStream in, @NonNull OutputStream out)125     public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
126         return android.os.FileUtils.copy(in, out);
127     }
128 
buildPath(File base, String... segments)129     public static File buildPath(File base, String... segments) {
130         File cur = base;
131         for (String segment : segments) {
132             if (cur == null) {
133                 cur = new File(segment);
134             } else {
135                 cur = new File(cur, segment);
136             }
137         }
138         return cur;
139     }
140 
141     /**
142      * Delete older files in a directory until only those matching the given
143      * constraints remain.
144      *
145      * @param minCount Always keep at least this many files.
146      * @param minAgeMs Always keep files younger than this age, in milliseconds.
147      * @return if any files were deleted.
148      */
deleteOlderFiles(File dir, int minCount, long minAgeMs)149     public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) {
150         if (minCount < 0 || minAgeMs < 0) {
151             throw new IllegalArgumentException("Constraints must be positive or 0");
152         }
153 
154         final File[] files = dir.listFiles();
155         if (files == null) return false;
156 
157         // Sort with newest files first
158         Arrays.sort(files, new Comparator<File>() {
159             @Override
160             public int compare(File lhs, File rhs) {
161                 return Long.compare(rhs.lastModified(), lhs.lastModified());
162             }
163         });
164 
165         // Keep at least minCount files
166         boolean deleted = false;
167         for (int i = minCount; i < files.length; i++) {
168             final File file = files[i];
169 
170             // Keep files newer than minAgeMs
171             final long age = System.currentTimeMillis() - file.lastModified();
172             if (age > minAgeMs) {
173                 if (file.delete()) {
174                     Log.d(TAG, "Deleted old file " + file);
175                     deleted = true;
176                 }
177             }
178         }
179         return deleted;
180     }
181 
182     /**
183      * Shamelessly borrowed from {@code android.os.FileUtils}.
184      */
translateModeStringToPosix(String mode)185     public static int translateModeStringToPosix(String mode) {
186         // Sanity check for invalid chars
187         for (int i = 0; i < mode.length(); i++) {
188             switch (mode.charAt(i)) {
189                 case 'r':
190                 case 'w':
191                 case 't':
192                 case 'a':
193                     break;
194                 default:
195                     throw new IllegalArgumentException("Bad mode: " + mode);
196             }
197         }
198 
199         int res = 0;
200         if (mode.startsWith("rw")) {
201             res = O_RDWR | O_CREAT;
202         } else if (mode.startsWith("w")) {
203             res = O_WRONLY | O_CREAT;
204         } else if (mode.startsWith("r")) {
205             res = O_RDONLY;
206         } else {
207             throw new IllegalArgumentException("Bad mode: " + mode);
208         }
209         if (mode.indexOf('t') != -1) {
210             res |= O_TRUNC;
211         }
212         if (mode.indexOf('a') != -1) {
213             res |= O_APPEND;
214         }
215         return res;
216     }
217 
218     /**
219      * Shamelessly borrowed from {@code android.os.FileUtils}.
220      */
translateModePosixToString(int mode)221     public static String translateModePosixToString(int mode) {
222         String res = "";
223         if ((mode & O_ACCMODE) == O_RDWR) {
224             res = "rw";
225         } else if ((mode & O_ACCMODE) == O_WRONLY) {
226             res = "w";
227         } else if ((mode & O_ACCMODE) == O_RDONLY) {
228             res = "r";
229         } else {
230             throw new IllegalArgumentException("Bad mode: " + mode);
231         }
232         if ((mode & O_TRUNC) == O_TRUNC) {
233             res += "t";
234         }
235         if ((mode & O_APPEND) == O_APPEND) {
236             res += "a";
237         }
238         return res;
239     }
240 
241     /**
242      * Shamelessly borrowed from {@code android.os.FileUtils}.
243      */
translateModePosixToPfd(int mode)244     public static int translateModePosixToPfd(int mode) {
245         int res = 0;
246         if ((mode & O_ACCMODE) == O_RDWR) {
247             res = MODE_READ_WRITE;
248         } else if ((mode & O_ACCMODE) == O_WRONLY) {
249             res = MODE_WRITE_ONLY;
250         } else if ((mode & O_ACCMODE) == O_RDONLY) {
251             res = MODE_READ_ONLY;
252         } else {
253             throw new IllegalArgumentException("Bad mode: " + mode);
254         }
255         if ((mode & O_CREAT) == O_CREAT) {
256             res |= MODE_CREATE;
257         }
258         if ((mode & O_TRUNC) == O_TRUNC) {
259             res |= MODE_TRUNCATE;
260         }
261         if ((mode & O_APPEND) == O_APPEND) {
262             res |= MODE_APPEND;
263         }
264         return res;
265     }
266 
267     /**
268      * Shamelessly borrowed from {@code android.os.FileUtils}.
269      */
translateModePfdToPosix(int mode)270     public static int translateModePfdToPosix(int mode) {
271         int res = 0;
272         if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
273             res = O_RDWR;
274         } else if ((mode & MODE_WRITE_ONLY) == MODE_WRITE_ONLY) {
275             res = O_WRONLY;
276         } else if ((mode & MODE_READ_ONLY) == MODE_READ_ONLY) {
277             res = O_RDONLY;
278         } else {
279             throw new IllegalArgumentException("Bad mode: " + mode);
280         }
281         if ((mode & MODE_CREATE) == MODE_CREATE) {
282             res |= O_CREAT;
283         }
284         if ((mode & MODE_TRUNCATE) == MODE_TRUNCATE) {
285             res |= O_TRUNC;
286         }
287         if ((mode & MODE_APPEND) == MODE_APPEND) {
288             res |= O_APPEND;
289         }
290         return res;
291     }
292 
293     /**
294      * Shamelessly borrowed from {@code android.os.FileUtils}.
295      */
translateModeAccessToPosix(int mode)296     public static int translateModeAccessToPosix(int mode) {
297         if (mode == F_OK) {
298             // There's not an exact mapping, so we attempt a read-only open to
299             // determine if a file exists
300             return O_RDONLY;
301         } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) {
302             return O_RDWR;
303         } else if ((mode & R_OK) == R_OK) {
304             return O_RDONLY;
305         } else if ((mode & W_OK) == W_OK) {
306             return O_WRONLY;
307         } else {
308             throw new IllegalArgumentException("Bad mode: " + mode);
309         }
310     }
311 
312     /**
313      * Test if a file lives under the given directory, either as a direct child
314      * or a distant grandchild.
315      * <p>
316      * Both files <em>must</em> have been resolved using
317      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
318      * attacks.
319      *
320      * @hide
321      */
contains(File[] dirs, File file)322     public static boolean contains(File[] dirs, File file) {
323         for (File dir : dirs) {
324             if (contains(dir, file)) {
325                 return true;
326             }
327         }
328         return false;
329     }
330 
331     /** {@hide} */
contains(Collection<File> dirs, File file)332     public static boolean contains(Collection<File> dirs, File file) {
333         for (File dir : dirs) {
334             if (contains(dir, file)) {
335                 return true;
336             }
337         }
338         return false;
339     }
340 
341     /**
342      * Test if a file lives under the given directory, either as a direct child
343      * or a distant grandchild.
344      * <p>
345      * Both files <em>must</em> have been resolved using
346      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
347      * attacks.
348      *
349      * @hide
350      */
contains(File dir, File file)351     public static boolean contains(File dir, File file) {
352         if (dir == null || file == null) return false;
353         return contains(dir.getAbsolutePath(), file.getAbsolutePath());
354     }
355 
356     /**
357      * Test if a file lives under the given directory, either as a direct child
358      * or a distant grandchild.
359      * <p>
360      * Both files <em>must</em> have been resolved using
361      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
362      * attacks.
363      *
364      * @hide
365      */
contains(String dirPath, String filePath)366     public static boolean contains(String dirPath, String filePath) {
367         if (dirPath.equals(filePath)) {
368             return true;
369         }
370         if (!dirPath.endsWith("/")) {
371             dirPath += "/";
372         }
373         return filePath.startsWith(dirPath);
374     }
375 
376     /**
377      * Write {@link String} to the given {@link File}. Deletes any existing file
378      * when the argument is {@link Optional#empty()}.
379      */
writeString(@onNull File file, @NonNull Optional<String> value)380     public static void writeString(@NonNull File file, @NonNull Optional<String> value)
381             throws IOException {
382         if (value.isPresent()) {
383             Files.write(file.toPath(), value.get().getBytes(StandardCharsets.UTF_8));
384         } else {
385             file.delete();
386         }
387     }
388 
389     /**
390      * Read given {@link File} as a single {@link String}. Returns
391      * {@link Optional#empty()} when the file doesn't exist.
392      */
readString(@onNull File file)393     public static @NonNull Optional<String> readString(@NonNull File file) throws IOException {
394         try {
395             final String value = new String(Files.readAllBytes(file.toPath()),
396                     StandardCharsets.UTF_8);
397             return Optional.of(value);
398         } catch (NoSuchFileException e) {
399             return Optional.empty();
400         }
401     }
402 
403     /**
404      * Recursively walk the contents of the given {@link Path}, invoking the
405      * given {@link Consumer} for every file and directory encountered. This is
406      * typically used for recursively deleting a directory tree.
407      * <p>
408      * Gracefully attempts to process as much as possible in the face of any
409      * failures.
410      */
walkFileTreeContents(@onNull Path path, @NonNull Consumer<Path> operation)411     public static void walkFileTreeContents(@NonNull Path path, @NonNull Consumer<Path> operation) {
412         try {
413             Files.walkFileTree(path, new FileVisitor<Path>() {
414                 @Override
415                 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
416                     return FileVisitResult.CONTINUE;
417                 }
418 
419                 @Override
420                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
421                     if (!Objects.equals(path, file)) {
422                         operation.accept(file);
423                     }
424                     return FileVisitResult.CONTINUE;
425                 }
426 
427                 @Override
428                 public FileVisitResult visitFileFailed(Path file, IOException e) {
429                     Log.w(TAG, "Failed to visit " + file, e);
430                     return FileVisitResult.CONTINUE;
431                 }
432 
433                 @Override
434                 public FileVisitResult postVisitDirectory(Path dir, IOException e) {
435                     if (!Objects.equals(path, dir)) {
436                         operation.accept(dir);
437                     }
438                     return FileVisitResult.CONTINUE;
439                 }
440             });
441         } catch (IOException e) {
442             Log.w(TAG, "Failed to walk " + path, e);
443         }
444     }
445 
446     /**
447      * Recursively delete all contents inside the given directory. Gracefully
448      * attempts to delete as much as possible in the face of any failures.
449      *
450      * @deprecated if you're calling this from inside {@code MediaProvider}, you
451      *             likely want to call {@link #forEach} with a separate
452      *             invocation to invalidate FUSE entries.
453      */
454     @Deprecated
deleteContents(@onNull File dir)455     public static void deleteContents(@NonNull File dir) {
456         walkFileTreeContents(dir.toPath(), (path) -> {
457             path.toFile().delete();
458         });
459     }
460 
isValidFatFilenameChar(char c)461     private static boolean isValidFatFilenameChar(char c) {
462         if ((0x00 <= c && c <= 0x1f)) {
463             return false;
464         }
465         switch (c) {
466             case '"':
467             case '*':
468             case '/':
469             case ':':
470             case '<':
471             case '>':
472             case '?':
473             case '\\':
474             case '|':
475             case 0x7F:
476                 return false;
477             default:
478                 return true;
479         }
480     }
481 
482     /**
483      * Check if given filename is valid for a FAT filesystem.
484      *
485      * @hide
486      */
isValidFatFilename(String name)487     public static boolean isValidFatFilename(String name) {
488         return (name != null) && name.equals(buildValidFatFilename(name));
489     }
490 
491     /**
492      * Mutate the given filename to make it valid for a FAT filesystem,
493      * replacing any invalid characters with "_".
494      *
495      * @hide
496      */
buildValidFatFilename(String name)497     public static String buildValidFatFilename(String name) {
498         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
499             return "(invalid)";
500         }
501         final StringBuilder res = new StringBuilder(name.length());
502         for (int i = 0; i < name.length(); i++) {
503             final char c = name.charAt(i);
504             if (isValidFatFilenameChar(c)) {
505                 res.append(c);
506             } else {
507                 res.append('_');
508             }
509         }
510         // Even though vfat allows 255 UCS-2 chars, we might eventually write to
511         // ext4 through a FUSE layer, so use that limit.
512         trimFilename(res, 255);
513         return res.toString();
514     }
515 
516     /** {@hide} */
517     // @VisibleForTesting
trimFilename(String str, int maxBytes)518     public static String trimFilename(String str, int maxBytes) {
519         final StringBuilder res = new StringBuilder(str);
520         trimFilename(res, maxBytes);
521         return res.toString();
522     }
523 
524     /** {@hide} */
trimFilename(StringBuilder res, int maxBytes)525     private static void trimFilename(StringBuilder res, int maxBytes) {
526         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
527         if (raw.length > maxBytes) {
528             maxBytes -= 3;
529             while (raw.length > maxBytes) {
530                 res.deleteCharAt(res.length() / 2);
531                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
532             }
533             res.insert(res.length() / 2, "...");
534         }
535     }
536 
537     /** {@hide} */
buildUniqueFileWithExtension(File parent, String name, String ext)538     private static File buildUniqueFileWithExtension(File parent, String name, String ext)
539             throws FileNotFoundException {
540         final Iterator<String> names = buildUniqueNameIterator(parent, name);
541         while (names.hasNext()) {
542             File file = buildFile(parent, names.next(), ext);
543             if (!file.exists()) {
544                 return file;
545             }
546         }
547         throw new FileNotFoundException("Failed to create unique file");
548     }
549 
550     private static final Pattern PATTERN_DCF_STRICT = Pattern
551             .compile("([A-Z0-9_]{4})([0-9]{4})");
552     private static final Pattern PATTERN_DCF_RELAXED = Pattern
553             .compile("((?:IMG|MVIMG|VID)_[0-9]{8}_[0-9]{6})(?:~([0-9]+))?");
554 
isDcim(@onNull File dir)555     private static boolean isDcim(@NonNull File dir) {
556         while (dir != null) {
557             if (Objects.equals("DCIM", dir.getName())) {
558                 return true;
559             }
560             dir = dir.getParentFile();
561         }
562         return false;
563     }
564 
buildUniqueNameIterator(@onNull File parent, @NonNull String name)565     private static @NonNull Iterator<String> buildUniqueNameIterator(@NonNull File parent,
566             @NonNull String name) {
567         if (isDcim(parent)) {
568             final Matcher dcfStrict = PATTERN_DCF_STRICT.matcher(name);
569             if (dcfStrict.matches()) {
570                 // Generate names like "IMG_1001"
571                 final String prefix = dcfStrict.group(1);
572                 return new Iterator<String>() {
573                     int i = Integer.parseInt(dcfStrict.group(2));
574                     @Override
575                     public String next() {
576                         final String res = String.format("%s%04d", prefix, i);
577                         i++;
578                         return res;
579                     }
580                     @Override
581                     public boolean hasNext() {
582                         return i <= 9999;
583                     }
584                 };
585             }
586 
587             final Matcher dcfRelaxed = PATTERN_DCF_RELAXED.matcher(name);
588             if (dcfRelaxed.matches()) {
589                 // Generate names like "IMG_20190102_030405~2"
590                 final String prefix = dcfRelaxed.group(1);
591                 return new Iterator<String>() {
592                     int i = TextUtils.isEmpty(dcfRelaxed.group(2)) ? 1
593                             : Integer.parseInt(dcfRelaxed.group(2));
594                     @Override
595                     public String next() {
596                         final String res = (i == 1) ? prefix : String.format("%s~%d", prefix, i);
597                         i++;
598                         return res;
599                     }
600                     @Override
601                     public boolean hasNext() {
602                         return i <= 99;
603                     }
604                 };
605             }
606         }
607 
608         // Generate names like "foo (2)"
609         return new Iterator<String>() {
610             int i = 0;
611             @Override
612             public String next() {
613                 final String res = (i == 0) ? name : name + " (" + i + ")";
614                 i++;
615                 return res;
616             }
617             @Override
618             public boolean hasNext() {
619                 return i < 32;
620             }
621         };
622     }
623 
624     /**
625      * Generates a unique file name under the given parent directory. If the display name doesn't
626      * have an extension that matches the requested MIME type, the default extension for that MIME
627      * type is appended. If a file already exists, the name is appended with a numerical value to
628      * make it unique.
629      *
630      * For example, the display name 'example' with 'text/plain' MIME might produce
631      * 'example.txt' or 'example (1).txt', etc.
632      *
633      * @throws FileNotFoundException
634      * @hide
635      */
636     public static File buildUniqueFile(File parent, String mimeType, String displayName)
637             throws FileNotFoundException {
638         final String[] parts = splitFileName(mimeType, displayName);
639         return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
640     }
641 
642     /** {@hide} */
643     public static File buildNonUniqueFile(File parent, String mimeType, String displayName) {
644         final String[] parts = splitFileName(mimeType, displayName);
645         return buildFile(parent, parts[0], parts[1]);
646     }
647 
648     /**
649      * Generates a unique file name under the given parent directory, keeping
650      * any extension intact.
651      *
652      * @hide
653      */
654     public static File buildUniqueFile(File parent, String displayName)
655             throws FileNotFoundException {
656         final String name;
657         final String ext;
658 
659         // Extract requested extension from display name
660         final int lastDot = displayName.lastIndexOf('.');
661         if (lastDot >= 0) {
662             name = displayName.substring(0, lastDot);
663             ext = displayName.substring(lastDot + 1);
664         } else {
665             name = displayName;
666             ext = null;
667         }
668 
669         return buildUniqueFileWithExtension(parent, name, ext);
670     }
671 
672     /**
673      * Splits file name into base name and extension.
674      * If the display name doesn't have an extension that matches the requested MIME type, the
675      * extension is regarded as a part of filename and default extension for that MIME type is
676      * appended.
677      *
678      * @hide
679      */
680     public static String[] splitFileName(String mimeType, String displayName) {
681         String name;
682         String ext;
683 
684         {
685             String mimeTypeFromExt;
686 
687             // Extract requested extension from display name
688             final int lastDot = displayName.lastIndexOf('.');
689             if (lastDot > 0) {
690                 name = displayName.substring(0, lastDot);
691                 ext = displayName.substring(lastDot + 1);
692                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
693                         ext.toLowerCase(Locale.ROOT));
694             } else {
695                 name = displayName;
696                 ext = null;
697                 mimeTypeFromExt = null;
698             }
699 
700             if (mimeTypeFromExt == null) {
701                 mimeTypeFromExt = ClipDescription.MIMETYPE_UNKNOWN;
702             }
703 
704             final String extFromMimeType;
705             if (ClipDescription.MIMETYPE_UNKNOWN.equalsIgnoreCase(mimeType)) {
706                 extFromMimeType = null;
707             } else {
708                 extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
709             }
710 
711             if (MimeUtils.equalIgnoreCase(mimeType, mimeTypeFromExt)
712                     || MimeUtils.equalIgnoreCase(ext, extFromMimeType)) {
713                 // Extension maps back to requested MIME type; allow it
714             } else {
715                 // No match; insist that create file matches requested MIME
716                 name = displayName;
717                 ext = extFromMimeType;
718             }
719         }
720 
721         if (ext == null) {
722             ext = "";
723         }
724 
725         return new String[] { name, ext };
726     }
727 
728     /** {@hide} */
729     private static File buildFile(File parent, String name, String ext) {
730         if (TextUtils.isEmpty(ext)) {
731             return new File(parent, name);
732         } else {
733             return new File(parent, name + "." + ext);
734         }
735     }
736 
737     public static @Nullable String extractDisplayName(@Nullable String data) {
738         if (data == null) return null;
739         if (data.indexOf('/') == -1) {
740             return data;
741         }
742         if (data.endsWith("/")) {
743             data = data.substring(0, data.length() - 1);
744         }
745         return data.substring(data.lastIndexOf('/') + 1);
746     }
747 
748     public static @Nullable String extractFileName(@Nullable String data) {
749         if (data == null) return null;
750         data = extractDisplayName(data);
751 
752         final int lastDot = data.lastIndexOf('.');
753         if (lastDot == -1) {
754             return data;
755         } else {
756             return data.substring(0, lastDot);
757         }
758     }
759 
760     public static @Nullable String extractFileExtension(@Nullable String data) {
761         if (data == null) return null;
762         data = extractDisplayName(data);
763 
764         final int lastDot = data.lastIndexOf('.');
765         if (lastDot == -1) {
766             return null;
767         } else {
768             return data.substring(lastDot + 1);
769         }
770     }
771 
772     /**
773      * Return list of paths that should be scanned with
774      * {@link com.android.providers.media.scan.MediaScanner} for the given
775      * volume name.
776      */
777     public static @NonNull Collection<File> getVolumeScanPaths(@NonNull Context context,
778             @NonNull String volumeName) throws FileNotFoundException {
779         final ArrayList<File> res = new ArrayList<>();
780         switch (volumeName) {
781             case MediaStore.VOLUME_INTERNAL: {
782                 res.addAll(Environment.getInternalMediaDirectories());
783                 break;
784             }
785             case MediaStore.VOLUME_EXTERNAL: {
786                 for (String resolvedVolumeName : MediaStore.getExternalVolumeNames(context)) {
787                     res.add(getVolumePath(context, resolvedVolumeName));
788                 }
789                 break;
790             }
791             default: {
792                 res.add(getVolumePath(context, volumeName));
793             }
794         }
795         return res;
796     }
797 
798     /**
799      * Return path where the given volume name is mounted.
800      */
801     public static @NonNull File getVolumePath(@NonNull Context context,
802             @NonNull String volumeName) throws FileNotFoundException {
803         switch (volumeName) {
804             case MediaStore.VOLUME_INTERNAL:
805             case MediaStore.VOLUME_EXTERNAL:
806                 throw new FileNotFoundException(volumeName + " has no associated path");
807         }
808 
809         final Uri uri = MediaStore.Files.getContentUri(volumeName);
810         final File path = context.getSystemService(StorageManager.class).getStorageVolume(uri)
811                 .getDirectory();
812         if (path != null) {
813             return path;
814         } else {
815             throw new FileNotFoundException(volumeName + " has no associated path");
816         }
817     }
818 
819     /**
820      * Returns the content URI for the volume that contains the given path.
821      *
822      * <p>{@link MediaStore.Files#getContentUriForPath(String)} can't detect public volumes and can
823      * only return the URI for the primary external storage, that's why this utility should be used
824      * instead.
825      */
826     public static @NonNull Uri getContentUriForPath(@NonNull String path) {
827         Objects.requireNonNull(path);
828         return MediaStore.Files.getContentUri(extractVolumeName(path));
829     }
830 
831     /**
832      * Return volume name which hosts the given path.
833      */
834     public static @NonNull String getVolumeName(@NonNull Context context, @NonNull File path) {
835         if (contains(Environment.getStorageDirectory(), path)) {
836             return context.getSystemService(StorageManager.class).getStorageVolume(path)
837                     .getMediaStoreVolumeName();
838         } else {
839             return MediaStore.VOLUME_INTERNAL;
840         }
841     }
842 
843     public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile(
844             "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/.+");
845     public static final Pattern PATTERN_DOWNLOADS_DIRECTORY = Pattern.compile(
846             "(?i)^/storage/[^/]+/(?:[0-9]+/)?(?:Android/sandbox/[^/]+/)?Download/?");
847     public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
848             "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$");
849     public static final Pattern PATTERN_PENDING_FILEPATH_FOR_SQL = Pattern.compile(
850             ".*/\\.pending-(\\d+)-([^/]+)$");
851 
852     /**
853      * File prefix indicating that the file {@link MediaColumns#IS_PENDING}.
854      */
855     public static final String PREFIX_PENDING = "pending";
856 
857     /**
858      * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}.
859      */
860     public static final String PREFIX_TRASHED = "trashed";
861 
862     /**
863      * Default duration that {@link MediaColumns#IS_PENDING} items should be
864      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
865      */
866     public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS;
867 
868     /**
869      * Default duration that {@link MediaColumns#IS_TRASHED} items should be
870      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
871      */
872     public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS;
873 
874     public static boolean isDownload(@NonNull String path) {
875         return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
876     }
877 
878     public static boolean isDownloadDir(@NonNull String path) {
879         return PATTERN_DOWNLOADS_DIRECTORY.matcher(path).matches();
880     }
881 
882     /**
883      * Regex that matches paths in all well-known package-specific directories,
884      * and which captures the package name as the first group.
885      */
886     public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
887             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?");
888 
889     /**
890      * Regex that matches Android/obb or Android/data path.
891      */
892     public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
893             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$");
894 
895     /**
896      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it
897      * captures both top-level paths and sandboxed paths.
898      */
899     private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
900             "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?");
901 
902     /**
903      * Regex that matches paths under well-known storage paths.
904      */
905     private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
906             "(?i)^/storage/([^/]+)");
907 
908     private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
909         return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
910     }
911 
912     public static @Nullable String extractVolumePath(@Nullable String data) {
913         if (data == null) return null;
914         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
915         if (matcher.find()) {
916             return data.substring(0, matcher.end());
917         } else {
918             return null;
919         }
920     }
921 
922     public static @Nullable String extractVolumeName(@Nullable String data) {
923         if (data == null) return null;
924         final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
925         if (matcher.find()) {
926             final String volumeName = matcher.group(1);
927             if (volumeName.equals("emulated")) {
928                 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
929             } else {
930                 return normalizeUuid(volumeName);
931             }
932         } else {
933             return MediaStore.VOLUME_INTERNAL;
934         }
935     }
936 
937     public static @Nullable String extractRelativePath(@Nullable String data) {
938         if (data == null) return null;
939         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
940         if (matcher.find()) {
941             final int lastSlash = data.lastIndexOf('/');
942             if (lastSlash == -1 || lastSlash < matcher.end()) {
943                 // This is a file in the top-level directory, so relative path is "/"
944                 // which is different than null, which means unknown path
945                 return "/";
946             } else {
947                 return data.substring(matcher.end(), lastSlash + 1);
948             }
949         } else {
950             return null;
951         }
952     }
953 
954     /**
955      * Returns relative path for the directory.
956      */
957     @VisibleForTesting
958     public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
959         if (directoryPath == null) return null;
960 
961         if (directoryPath.equals("/storage/emulated") ||
962                 directoryPath.equals("/storage/emulated/")) {
963             // This path is not reachable for MediaProvider.
964             return null;
965         }
966 
967         // We are extracting relative path for the directory itself, we add "/" so that we can use
968         // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
969         // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
970         // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
971         if (!directoryPath.endsWith("/")) {
972             // Relative path for directory should end with "/".
973             directoryPath += "/";
974         }
975 
976         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
977         if (matcher.find()) {
978             if (matcher.end() == directoryPath.length()) {
979                 // This is the top-level directory, so relative path is "/"
980                 return "/";
981             }
982             return directoryPath.substring(matcher.end());
983         }
984         return null;
985     }
986 
987     public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
988         if (path == null) return null;
989         final Matcher m = PATTERN_OWNED_PATH.matcher(path);
990         if (m.matches()) {
991             return m.group(1);
992         } else {
993             return null;
994         }
995     }
996 
997     /**
998      * Returns true if relative path is Android/data or Android/obb path.
999      */
1000     public static boolean isDataOrObbPath(String path) {
1001         if (path == null) return false;
1002         final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
1003         return m.matches();
1004     }
1005 
1006     /**
1007      * Returns the name of the top level directory, or null if the path doesn't go through the
1008      * external storage directory.
1009      */
1010     @Nullable
1011     public static String extractTopLevelDir(String path) {
1012         final String relativePath = extractRelativePath(path);
1013         if (relativePath == null) {
1014             return null;
1015         }
1016         final String[] relativePathSegments = relativePath.split("/");
1017         return relativePathSegments.length > 0 ? relativePathSegments[0] : null;
1018     }
1019 
1020     /**
1021      * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
1022      * columns being modified by this operation.
1023      */
1024     public static void computeDateExpires(@NonNull ContentValues values) {
1025         // External apps have no ability to change this field
1026         values.remove(MediaColumns.DATE_EXPIRES);
1027 
1028         // Only define the field when this modification is actually adjusting
1029         // one of the flags that should influence the expiration
1030         final Object pending = values.get(MediaColumns.IS_PENDING);
1031         if (pending != null) {
1032             if (parseBoolean(pending, false)) {
1033                 values.put(MediaColumns.DATE_EXPIRES,
1034                         (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1035             } else {
1036                 values.putNull(MediaColumns.DATE_EXPIRES);
1037             }
1038         }
1039         final Object trashed = values.get(MediaColumns.IS_TRASHED);
1040         if (trashed != null) {
1041             if (parseBoolean(trashed, false)) {
1042                 values.put(MediaColumns.DATE_EXPIRES,
1043                         (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1044             } else {
1045                 values.putNull(MediaColumns.DATE_EXPIRES);
1046             }
1047         }
1048     }
1049 
1050     /**
1051      * Compute several scattered {@link MediaColumns} values from
1052      * {@link MediaColumns#DATA}. This method performs no enforcement of
1053      * argument validity.
1054      */
1055     public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) {
1056         // Worst case we have to assume no bucket details
1057         values.remove(MediaColumns.VOLUME_NAME);
1058         values.remove(MediaColumns.RELATIVE_PATH);
1059         values.remove(MediaColumns.IS_TRASHED);
1060         values.remove(MediaColumns.DATE_EXPIRES);
1061         values.remove(MediaColumns.DISPLAY_NAME);
1062         values.remove(MediaColumns.BUCKET_ID);
1063         values.remove(MediaColumns.BUCKET_DISPLAY_NAME);
1064 
1065         final String data = values.getAsString(MediaColumns.DATA);
1066         if (TextUtils.isEmpty(data)) return;
1067 
1068         final File file = new File(data);
1069         final File fileLower = new File(data.toLowerCase(Locale.ROOT));
1070 
1071         values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
1072         values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
1073         final String displayName = extractDisplayName(data);
1074         final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
1075         if (matcher.matches()) {
1076             values.put(MediaColumns.IS_PENDING,
1077                     matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0);
1078             values.put(MediaColumns.IS_TRASHED,
1079                     matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0);
1080             values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2)));
1081             values.put(MediaColumns.DISPLAY_NAME, matcher.group(3));
1082         } else {
1083             if (isForFuse) {
1084                 // Allow Fuse thread to set IS_PENDING when using DATA column.
1085                 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify
1086                 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't
1087                 // explicitly specify the value of IS_PENDING.
1088             } else {
1089                 values.put(MediaColumns.IS_PENDING, 0);
1090             }
1091             values.put(MediaColumns.IS_TRASHED, 0);
1092             values.putNull(MediaColumns.DATE_EXPIRES);
1093             values.put(MediaColumns.DISPLAY_NAME, displayName);
1094         }
1095 
1096         // Buckets are the parent directory
1097         final String parent = fileLower.getParent();
1098         if (parent != null) {
1099             values.put(MediaColumns.BUCKET_ID, parent.hashCode());
1100             // The relative path for files in the top directory is "/"
1101             if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) {
1102                 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
1103             }
1104         }
1105     }
1106 
1107     /**
1108      * Compute {@link MediaColumns#DATA} from several scattered
1109      * {@link MediaColumns} values.  This method performs no enforcement of
1110      * argument validity.
1111      */
1112     public static void computeDataFromValues(@NonNull ContentValues values,
1113             @NonNull File volumePath, boolean isForFuse) {
1114         values.remove(MediaColumns.DATA);
1115 
1116         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1117         final String resolvedDisplayName;
1118         // Pending file path shouldn't be rewritten for files inserted via filepath.
1119         if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
1120             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1121                     (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1122             resolvedDisplayName = String.format(".%s-%d-%s",
1123                     FileUtils.PREFIX_PENDING, dateExpires, displayName);
1124         } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
1125             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1126                     (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1127             resolvedDisplayName = String.format(".%s-%d-%s",
1128                     FileUtils.PREFIX_TRASHED, dateExpires, displayName);
1129         } else {
1130             resolvedDisplayName = displayName;
1131         }
1132 
1133         final File filePath = buildPath(volumePath,
1134                 values.getAsString(MediaColumns.RELATIVE_PATH), resolvedDisplayName);
1135         values.put(MediaColumns.DATA, filePath.getAbsolutePath());
1136     }
1137 
1138     public static void sanitizeValues(@NonNull ContentValues values,
1139             boolean rewriteHiddenFileName) {
1140         final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
1141         for (int i = 0; i < relativePath.length; i++) {
1142             relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName);
1143         }
1144         values.put(MediaColumns.RELATIVE_PATH,
1145                 String.join("/", relativePath) + "/");
1146 
1147         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1148         values.put(MediaColumns.DISPLAY_NAME,
1149                 sanitizeDisplayName(displayName, rewriteHiddenFileName));
1150     }
1151 
1152     /** {@hide} **/
1153     @Nullable
1154     public static String getAbsoluteSanitizedPath(String path) {
1155         final String[] pathSegments = sanitizePath(path);
1156         if (pathSegments.length == 0) {
1157             return null;
1158         }
1159         return path = "/" + String.join("/",
1160                 Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
1161     }
1162 
1163     /** {@hide} */
1164     public static @NonNull String[] sanitizePath(@Nullable String path) {
1165         if (path == null) {
1166             return new String[0];
1167         } else {
1168             final String[] segments = path.split("/");
1169             // If the path corresponds to the top level directory, then we return an empty path
1170             // which denotes the top level directory
1171             if (segments.length == 0) {
1172                 return new String[] { "" };
1173             }
1174             for (int i = 0; i < segments.length; i++) {
1175                 segments[i] = sanitizeDisplayName(segments[i]);
1176             }
1177             return segments;
1178         }
1179     }
1180 
1181     /**
1182      * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem.
1183      * @hide
1184      */
1185     public static @Nullable String sanitizeDisplayName(@Nullable String name) {
1186         return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false);
1187     }
1188 
1189     /**
1190      * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to
1191      * make it valid for a FAT filesystem.
1192      * @hide
1193      */
1194     public static @Nullable String sanitizeDisplayName(@Nullable String name,
1195             boolean rewriteHiddenFileName) {
1196         if (name == null) {
1197             return null;
1198         } else if (rewriteHiddenFileName && name.startsWith(".")) {
1199             // The resulting file must not be hidden.
1200             return "_" + name;
1201         } else {
1202             return buildValidFatFilename(name);
1203         }
1204     }
1205 
1206     /**
1207      * Test if this given directory should be considered hidden.
1208      */
1209     @VisibleForTesting
1210     public static boolean isDirectoryHidden(@NonNull File dir) {
1211         final String name = dir.getName();
1212         if (name.startsWith(".")) {
1213             return true;
1214         }
1215 
1216         final File nomedia = new File(dir, ".nomedia");
1217         // check for .nomedia presence
1218         if (nomedia.exists()) {
1219             Logging.logPersistent("Observed non-standard " + nomedia);
1220             return true;
1221         }
1222         return false;
1223     }
1224 
1225     /**
1226      * Test if this given file should be considered hidden.
1227      */
1228     @VisibleForTesting
1229     public static boolean isFileHidden(@NonNull File file) {
1230         final String name = file.getName();
1231 
1232         // Handle well-known file names that are pending or trashed; they
1233         // normally appear hidden, but we give them special treatment
1234         if (PATTERN_EXPIRES_FILE.matcher(name).matches()) {
1235             return false;
1236         }
1237 
1238         // Otherwise fall back to file name
1239         if (name.startsWith(".")) {
1240             return true;
1241         }
1242         return false;
1243     }
1244 
1245     /**
1246      * Clears all app's external cache directories, i.e. for each app we delete
1247      * /sdcard/Android/data/app/cache/* but we keep the directory itself.
1248      *
1249      * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs.
1250      *
1251      * <p>This method doesn't perform any checks, so make sure that the calling package is allowed
1252      * to clear cache directories first.
1253      *
1254      * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none
1255      * or part of the directories were cleared.
1256      */
1257     public static int clearAppCacheDirectories() {
1258         int status = 0;
1259         Log.i(TAG, "Clearing cache for all apps");
1260         final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(),
1261                 "Android", "data");
1262         for (File appDataDir : rootDataDir.listFiles()) {
1263             try {
1264                 final File appCacheDir = new File(appDataDir, "cache");
1265                 if (appCacheDir.isDirectory()) {
1266                     FileUtils.deleteContents(appCacheDir);
1267                 }
1268             } catch (Exception e) {
1269                 // We want to avoid crashing MediaProvider at all costs, so we handle all "generic"
1270                 // exceptions here, and just report to the caller that an IO exception has occurred.
1271                 // We still try to clear the rest of the directories.
1272                 Log.e(TAG, "Couldn't delete all app cache dirs!", e);
1273                 status = OsConstants.EIO;
1274             }
1275         }
1276         return status;
1277     }
1278 }
1279