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