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