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                     + "(?:(?:Movies|Music|Pictures)/.thumbnails$))");
1008 
1009     /**
1010      * The recordings directory. This is used for R OS. For S OS or later,
1011      * we use {@link Environment#DIRECTORY_RECORDINGS} directly.
1012      */
1013     public static final String DIRECTORY_RECORDINGS = "Recordings";
1014 
1015     @VisibleForTesting
1016     public static final String[] DEFAULT_FOLDER_NAMES;
1017     static {
1018         if (SdkLevel.isAtLeastS()) {
1019             DEFAULT_FOLDER_NAMES = new String[]{
1020                     Environment.DIRECTORY_MUSIC,
1021                     Environment.DIRECTORY_PODCASTS,
1022                     Environment.DIRECTORY_RINGTONES,
1023                     Environment.DIRECTORY_ALARMS,
1024                     Environment.DIRECTORY_NOTIFICATIONS,
1025                     Environment.DIRECTORY_PICTURES,
1026                     Environment.DIRECTORY_MOVIES,
1027                     Environment.DIRECTORY_DOWNLOADS,
1028                     Environment.DIRECTORY_DCIM,
1029                     Environment.DIRECTORY_DOCUMENTS,
1030                     Environment.DIRECTORY_AUDIOBOOKS,
1031                     Environment.DIRECTORY_RECORDINGS,
1032             };
1033         } else {
1034             DEFAULT_FOLDER_NAMES = new String[]{
1035                     Environment.DIRECTORY_MUSIC,
1036                     Environment.DIRECTORY_PODCASTS,
1037                     Environment.DIRECTORY_RINGTONES,
1038                     Environment.DIRECTORY_ALARMS,
1039                     Environment.DIRECTORY_NOTIFICATIONS,
1040                     Environment.DIRECTORY_PICTURES,
1041                     Environment.DIRECTORY_MOVIES,
1042                     Environment.DIRECTORY_DOWNLOADS,
1043                     Environment.DIRECTORY_DCIM,
1044                     Environment.DIRECTORY_DOCUMENTS,
1045                     Environment.DIRECTORY_AUDIOBOOKS,
1046                     DIRECTORY_RECORDINGS,
1047             };
1048         }
1049     }
1050 
1051     /**
1052      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}
1053      */
1054     private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
1055             "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)");
1056 
1057     /**
1058      * Regex that matches paths under well-known storage paths.
1059      */
1060     private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
1061             "(?i)^/storage/([^/]+)");
1062 
1063     /**
1064      * Regex that matches user-ids under well-known storage paths.
1065      */
1066     private static final Pattern PATTERN_USER_ID = Pattern.compile(
1067             "(?i)^/storage/emulated/([0-9]+)");
1068 
1069     private static final String CAMERA_RELATIVE_PATH =
1070             String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera");
1071 
1072     public static boolean isCrossUserEnabled() {
1073         return PROP_CROSS_USER_ALLOWED || SdkLevel.isAtLeastS();
1074     }
1075 
1076     private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
1077         return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
1078     }
1079 
1080     public static int extractUserId(@Nullable String data) {
1081         if (data == null) return -1;
1082         final Matcher matcher = PATTERN_USER_ID.matcher(data);
1083         if (matcher.find()) {
1084             return Integer.parseInt(matcher.group(1));
1085         }
1086 
1087         return -1;
1088     }
1089 
1090     public static @Nullable String extractVolumePath(@Nullable String data) {
1091         if (data == null) return null;
1092         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
1093         if (matcher.find()) {
1094             return data.substring(0, matcher.end());
1095         } else {
1096             return null;
1097         }
1098     }
1099 
1100     public static @Nullable String extractVolumeName(@Nullable String data) {
1101         if (data == null) return null;
1102         final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
1103         if (matcher.find()) {
1104             final String volumeName = matcher.group(1);
1105             if (volumeName.equals("emulated")) {
1106                 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
1107             } else {
1108                 return normalizeUuid(volumeName);
1109             }
1110         } else {
1111             return MediaStore.VOLUME_INTERNAL;
1112         }
1113     }
1114 
1115     public static @Nullable String extractRelativePath(@Nullable String data) {
1116         if (data == null) return null;
1117 
1118         final String path;
1119         try {
1120             path = getCanonicalPath(data);
1121         } catch (IOException e) {
1122             Log.d(TAG, "Unable to get canonical path from invalid data path: " + data, e);
1123             return null;
1124         }
1125 
1126         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path);
1127         if (matcher.find()) {
1128             final int lastSlash = path.lastIndexOf('/');
1129             if (lastSlash == -1 || lastSlash < matcher.end()) {
1130                 // This is a file in the top-level directory, so relative path is "/"
1131                 // which is different than null, which means unknown path
1132                 return "/";
1133             } else {
1134                 return path.substring(matcher.end(), lastSlash + 1);
1135             }
1136         } else {
1137             return null;
1138         }
1139     }
1140 
1141     /**
1142      * Returns relative path with display name.
1143      */
1144     @VisibleForTesting
1145     public static @Nullable String extractRelativePathWithDisplayName(@Nullable String path) {
1146         if (path == null) return null;
1147 
1148         if (path.equals("/storage/emulated") || path.equals("/storage/emulated/")) {
1149             // This path is not reachable for MediaProvider.
1150             return null;
1151         }
1152 
1153         // We are extracting relative path for the directory itself, we add "/" so that we can use
1154         // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
1155         // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
1156         // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
1157         if (!path.endsWith("/")) {
1158             // Relative path for directory should end with "/".
1159             path += "/";
1160         }
1161 
1162         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path);
1163         if (matcher.find()) {
1164             if (matcher.end() == path.length()) {
1165                 // This is the top-level directory, so relative path is "/"
1166                 return "/";
1167             }
1168             return path.substring(matcher.end());
1169         }
1170         return null;
1171     }
1172 
1173     public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
1174         if (path == null) return null;
1175         final Matcher m = PATTERN_OWNED_PATH.matcher(path);
1176         if (m.matches()) {
1177             return m.group(1);
1178         }
1179         return null;
1180     }
1181 
1182     public static @Nullable String extractOwnerPackageNameFromRelativePath(@Nullable String path) {
1183         if (path == null) return null;
1184         final Matcher m = PATTERN_OWNED_RELATIVE_PATH.matcher(path);
1185         if (m.matches()) {
1186             return m.group(1);
1187         }
1188         return null;
1189     }
1190 
1191     public static boolean isExternalMediaDirectory(@NonNull String path) {
1192         return isExternalMediaDirectory(path, PROP_CROSS_USER_ROOT);
1193     }
1194 
1195     @VisibleForTesting
1196     static boolean isExternalMediaDirectory(@NonNull String path, String crossUserRoot) {
1197         final String relativePath = extractRelativePath(path);
1198         if (relativePath == null) {
1199             return false;
1200         }
1201 
1202         if (StringUtils.startsWithIgnoreCase(relativePath, "Android/media")) {
1203             return true;
1204         }
1205         if (!TextUtils.isEmpty(crossUserRoot)) {
1206             return StringUtils.startsWithIgnoreCase(relativePath, crossUserRoot + "/Android/media");
1207         }
1208 
1209         return false;
1210     }
1211 
1212     /**
1213      * Returns true if path is Android/data or Android/obb path.
1214      */
1215     public static boolean isDataOrObbPath(@Nullable String path) {
1216         if (path == null) return false;
1217         final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
1218         return m.matches();
1219     }
1220 
1221     /**
1222      * Returns true if relative path is Android/data or Android/obb path.
1223      */
1224     public static boolean isDataOrObbRelativePath(@Nullable String path) {
1225         if (path == null) return false;
1226         final Matcher m = PATTERN_DATA_OR_OBB_RELATIVE_PATH.matcher(path);
1227         return m.matches();
1228     }
1229 
1230     /**
1231      * Returns true if relative path is Android/obb path.
1232      */
1233     public static boolean isObbOrChildRelativePath(@Nullable String path) {
1234         if (path == null) return false;
1235         final Matcher m = PATTERN_OBB_OR_CHILD_RELATIVE_PATH.matcher(path);
1236         return m.matches();
1237     }
1238 
1239     public static boolean shouldBeVisible(@Nullable String path) {
1240         if (path == null) return false;
1241         final Matcher m = PATTERN_VISIBLE.matcher(path);
1242         return m.matches();
1243     }
1244 
1245     public static boolean shouldBeInvisible(@Nullable String path) {
1246         if (path == null) return false;
1247         final Matcher m = PATTERN_INVISIBLE.matcher(path);
1248         return m.matches();
1249     }
1250 
1251     /**
1252      * Returns the name of the top level directory, or null if the path doesn't go through the
1253      * external storage directory.
1254      */
1255     @Nullable
1256     public static String extractTopLevelDir(String path) {
1257         final String relativePath = extractRelativePath(path);
1258         if (relativePath == null) {
1259             return null;
1260         }
1261 
1262         return extractTopLevelDir(relativePath.split("/"));
1263     }
1264 
1265     @Nullable
1266     public static String extractTopLevelDir(String[] relativePathSegments) {
1267         return extractTopLevelDir(relativePathSegments, PROP_CROSS_USER_ROOT);
1268     }
1269 
1270     @VisibleForTesting
1271     @Nullable
1272     static String extractTopLevelDir(String[] relativePathSegments, String crossUserRoot) {
1273         if (relativePathSegments == null) return null;
1274 
1275         final String topLevelDir = relativePathSegments.length > 0 ? relativePathSegments[0] : null;
1276         if (crossUserRoot != null && crossUserRoot.equals(topLevelDir)) {
1277             return relativePathSegments.length > 1 ? relativePathSegments[1] : null;
1278         }
1279 
1280         return topLevelDir;
1281     }
1282 
1283     public static boolean isDefaultDirectoryName(@Nullable String dirName) {
1284         for (String defaultDirName : DEFAULT_FOLDER_NAMES) {
1285             if (defaultDirName.equalsIgnoreCase(dirName)) {
1286                 return true;
1287             }
1288         }
1289         return false;
1290     }
1291 
1292     /**
1293      * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
1294      * columns being modified by this operation.
1295      */
1296     public static void computeDateExpires(@NonNull ContentValues values) {
1297         // External apps have no ability to change this field
1298         values.remove(MediaColumns.DATE_EXPIRES);
1299 
1300         // Only define the field when this modification is actually adjusting
1301         // one of the flags that should influence the expiration
1302         final Object pending = values.get(MediaColumns.IS_PENDING);
1303         if (pending != null) {
1304             if (parseBoolean(pending, false)) {
1305                 values.put(MediaColumns.DATE_EXPIRES,
1306                         (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1307             } else {
1308                 values.putNull(MediaColumns.DATE_EXPIRES);
1309             }
1310         }
1311         final Object trashed = values.get(MediaColumns.IS_TRASHED);
1312         if (trashed != null) {
1313             if (parseBoolean(trashed, false)) {
1314                 values.put(MediaColumns.DATE_EXPIRES,
1315                         (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1316             } else {
1317                 values.putNull(MediaColumns.DATE_EXPIRES);
1318             }
1319         }
1320     }
1321 
1322     /**
1323      * Compute several scattered {@link MediaColumns} values from
1324      * {@link MediaColumns#DATA}. This method performs no enforcement of
1325      * argument validity.
1326      */
1327     public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) {
1328         // Worst case we have to assume no bucket details
1329         values.remove(MediaColumns.VOLUME_NAME);
1330         values.remove(MediaColumns.RELATIVE_PATH);
1331         values.remove(MediaColumns.IS_TRASHED);
1332         values.remove(MediaColumns.DATE_EXPIRES);
1333         values.remove(MediaColumns.DISPLAY_NAME);
1334         values.remove(MediaColumns.BUCKET_ID);
1335         values.remove(MediaColumns.BUCKET_DISPLAY_NAME);
1336 
1337         String data = values.getAsString(MediaColumns.DATA);
1338         if (TextUtils.isEmpty(data)) return;
1339 
1340         try {
1341             data = new File(data).getCanonicalPath();
1342             values.put(MediaColumns.DATA, data);
1343         } catch (IOException e) {
1344             throw new IllegalArgumentException(
1345                     String.format(Locale.ROOT, "Invalid file path:%s in request.", data));
1346         }
1347 
1348         final File file = new File(data);
1349         final File fileLower = new File(data.toLowerCase(Locale.ROOT));
1350 
1351         values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
1352         values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
1353         final String displayName = extractDisplayName(data);
1354         final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
1355         if (matcher.matches()) {
1356             values.put(MediaColumns.IS_PENDING,
1357                     matcher.group(1).equals(FileUtils.PREFIX_PENDING) ? 1 : 0);
1358             values.put(MediaColumns.IS_TRASHED,
1359                     matcher.group(1).equals(FileUtils.PREFIX_TRASHED) ? 1 : 0);
1360             values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2)));
1361             values.put(MediaColumns.DISPLAY_NAME, matcher.group(3));
1362         } else {
1363             if (isForFuse) {
1364                 // Allow Fuse thread to set IS_PENDING when using DATA column.
1365                 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify
1366                 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't
1367                 // explicitly specify the value of IS_PENDING.
1368             } else {
1369                 values.put(MediaColumns.IS_PENDING, 0);
1370             }
1371             values.put(MediaColumns.IS_TRASHED, 0);
1372             values.putNull(MediaColumns.DATE_EXPIRES);
1373             values.put(MediaColumns.DISPLAY_NAME, displayName);
1374         }
1375 
1376         // Buckets are the parent directory
1377         final String parent = fileLower.getParent();
1378         if (parent != null) {
1379             values.put(MediaColumns.BUCKET_ID, parent.hashCode());
1380             // The relative path for files in the top directory is "/"
1381             if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) {
1382                 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
1383             } else {
1384                 values.putNull(MediaColumns.BUCKET_DISPLAY_NAME);
1385             }
1386         }
1387     }
1388 
1389     /**
1390      * Compute {@link MediaColumns#DATA} from several scattered
1391      * {@link MediaColumns} values.  This method performs no enforcement of
1392      * argument validity.
1393      */
1394     public static void computeDataFromValues(@NonNull ContentValues values,
1395             @NonNull File volumePath, boolean isForFuse) {
1396         values.remove(MediaColumns.DATA);
1397 
1398         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1399         final String resolvedDisplayName;
1400         // Pending file path shouldn't be rewritten for files inserted via filepath.
1401         if (!isForFuse && getAsBoolean(values, MediaColumns.IS_PENDING, false)) {
1402             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1403                     (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
1404             final String combinedString = String.format(
1405                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_PENDING, dateExpires, displayName);
1406             // trim the file name to avoid ENAMETOOLONG error
1407             // after trim the file, if the user unpending the file,
1408             // the file name is not the original one
1409             resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
1410         } else if (getAsBoolean(values, MediaColumns.IS_TRASHED, false)) {
1411             final long dateExpires = getAsLong(values, MediaColumns.DATE_EXPIRES,
1412                     (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
1413             final String combinedString = String.format(
1414                     Locale.US, ".%s-%d-%s", FileUtils.PREFIX_TRASHED, dateExpires, displayName);
1415             // trim the file name to avoid ENAMETOOLONG error
1416             // after trim the file, if the user untrashes the file,
1417             // the file name is not the original one
1418             resolvedDisplayName = trimFilename(combinedString, MAX_FILENAME_BYTES);
1419         } else {
1420             resolvedDisplayName = displayName;
1421         }
1422 
1423         String relativePath = values.getAsString(MediaColumns.RELATIVE_PATH);
1424         if (relativePath == null) {
1425           relativePath = "";
1426         }
1427         try {
1428             final File filePath = buildPath(volumePath, relativePath, resolvedDisplayName);
1429             values.put(MediaColumns.DATA, filePath.getCanonicalPath());
1430         } catch (IOException e) {
1431             throw new IllegalArgumentException(
1432                     String.format("Failure in conversion to canonical file path. Failure path: %s.",
1433                             relativePath.concat(resolvedDisplayName)), e);
1434         }
1435     }
1436 
1437     @VisibleForTesting
1438     static ArrayMap<String, String> sAudioTypes = new ArrayMap<>();
1439 
1440     static {
1441         sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE);
1442         sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION);
1443         sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM);
1444         sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST);
1445         sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK);
1446         sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
1447         if (SdkLevel.isAtLeastS()) {
1448             sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
1449         } else {
1450             sAudioTypes.put(FileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
1451         }
1452     }
1453 
1454     /**
1455      * Compute values for columns in {@code sAudioTypes} based on the given {@code filePath}.
1456      */
1457     public static void computeAudioTypeValuesFromData(@NonNull String filePath,
1458             @NonNull ObjIntConsumer<String> consumer) {
1459         final String lowPath = filePath.toLowerCase(Locale.ROOT);
1460         boolean anyMatch = false;
1461         for (int i = 0; i < sAudioTypes.size(); i++) {
1462             final boolean match = lowPath
1463                     .contains('/' + sAudioTypes.keyAt(i).toLowerCase(Locale.ROOT) + '/');
1464             consumer.accept(sAudioTypes.valueAt(i), match ? 1 : 0);
1465             anyMatch |= match;
1466         }
1467         if (!anyMatch) {
1468             consumer.accept(AudioColumns.IS_MUSIC, 1);
1469         }
1470     }
1471 
1472     public static void sanitizeValues(@NonNull ContentValues values,
1473             boolean rewriteHiddenFileName) {
1474         final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
1475         for (int i = 0; i < relativePath.length; i++) {
1476             relativePath[i] = sanitizeDisplayName(relativePath[i], rewriteHiddenFileName);
1477         }
1478         values.put(MediaColumns.RELATIVE_PATH,
1479                 String.join("/", relativePath) + "/");
1480 
1481         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
1482         values.put(MediaColumns.DISPLAY_NAME,
1483                 sanitizeDisplayName(displayName, rewriteHiddenFileName));
1484     }
1485 
1486     /** {@hide} **/
1487     @Nullable
1488     public static String getAbsoluteSanitizedPath(String path) {
1489         final String[] pathSegments = sanitizePath(path);
1490         if (pathSegments.length == 0) {
1491             return null;
1492         }
1493         return path = "/" + String.join("/",
1494                 Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
1495     }
1496 
1497     /** {@hide} */
1498     public static @NonNull String[] sanitizePath(@Nullable String path) {
1499         if (path == null) {
1500             return new String[0];
1501         } else {
1502             final String[] segments = path.split("/");
1503             // If the path corresponds to the top level directory, then we return an empty path
1504             // which denotes the top level directory
1505             if (segments.length == 0) {
1506                 return new String[] { "" };
1507             }
1508             for (int i = 0; i < segments.length; i++) {
1509                 segments[i] = sanitizeDisplayName(segments[i]);
1510             }
1511             return segments;
1512         }
1513     }
1514 
1515     /**
1516      * Sanitizes given name by mutating the file name to make it valid for a FAT filesystem.
1517      * @hide
1518      */
1519     public static @Nullable String sanitizeDisplayName(@Nullable String name) {
1520         return sanitizeDisplayName(name, /*rewriteHiddenFileName*/ false);
1521     }
1522 
1523     /**
1524      * Sanitizes given name by appending '_' to make it non-hidden and mutating the file name to
1525      * make it valid for a FAT filesystem.
1526      * @hide
1527      */
1528     public static @Nullable String sanitizeDisplayName(@Nullable String name,
1529             boolean rewriteHiddenFileName) {
1530         if (name == null) {
1531             return null;
1532         } else if (rewriteHiddenFileName && name.startsWith(".")) {
1533             // The resulting file must not be hidden.
1534             return "_" + name;
1535         } else {
1536             return buildValidFatFilename(name);
1537         }
1538     }
1539 
1540     /**
1541      * Returns true if the given File should be hidden (if it or any of its parents is hidden).
1542      * This can be called before the file is created, to check if it will be hidden once created.
1543      */
1544     @VisibleForTesting
1545     public static boolean shouldFileBeHidden(@NonNull File file) {
1546         if (isFileHidden(file)) {
1547             return true;
1548         }
1549 
1550         File parent = file.getParentFile();
1551         while (parent != null) {
1552             if (isDirectoryHidden(parent)) {
1553                 return true;
1554             }
1555             parent = parent.getParentFile();
1556         }
1557 
1558         return false;
1559     }
1560 
1561     /**
1562      * Returns true if the given dir should be hidden (if it or any of its parents is hidden).
1563      * This can be called before the file is created, to check if it will be hidden once created.
1564      */
1565     @VisibleForTesting
1566     public static boolean shouldDirBeHidden(@NonNull File file) {
1567         if (isDirectoryHidden(file)) {
1568             return true;
1569         }
1570 
1571         File parent = file.getParentFile();
1572         while (parent != null) {
1573             if (isDirectoryHidden(parent)) {
1574                 return true;
1575             }
1576             parent = parent.getParentFile();
1577         }
1578 
1579         return false;
1580     }
1581 
1582     /**
1583      * Test if this given directory should be considered hidden.
1584      */
1585     @VisibleForTesting
1586     public static boolean isDirectoryHidden(@NonNull File dir) {
1587         final String name = dir.getName();
1588         if (name.startsWith(".")) {
1589             return true;
1590         }
1591 
1592         final File nomedia = new File(dir, ".nomedia");
1593 
1594         // check for .nomedia presence
1595         if (!nomedia.exists()) {
1596             return false;
1597         }
1598 
1599         if (shouldBeVisible(dir.getAbsolutePath())) {
1600             nomedia.delete();
1601             return false;
1602         }
1603 
1604         // Handle top-level default directories. These directories should always be visible,
1605         // regardless of .nomedia presence.
1606         final String[] relativePath = sanitizePath(extractRelativePath(dir.getAbsolutePath()));
1607         final boolean isTopLevelDir =
1608                 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
1609         if (isTopLevelDir && isDefaultDirectoryName(name)) {
1610             nomedia.delete();
1611             return false;
1612         }
1613 
1614         // DCIM/Camera should always be visible regardless of .nomedia presence.
1615         if (CAMERA_RELATIVE_PATH.equalsIgnoreCase(
1616                 extractRelativePathWithDisplayName(dir.getAbsolutePath()))) {
1617             nomedia.delete();
1618             return false;
1619         }
1620 
1621         if (isScreenshotsDirNonHidden(relativePath, name)) {
1622             nomedia.delete();
1623             return false;
1624         }
1625 
1626         // .nomedia is present which makes this directory as hidden directory
1627         Logging.logPersistent("Observed non-standard " + nomedia);
1628         return true;
1629     }
1630 
1631     /**
1632      * Consider Screenshots directory in root directory or inside well-known directory as always
1633      * non-hidden. Nomedia file in these directories will not be able to hide these directories.
1634      * i.e., some examples of directories that will be considered non-hidden are
1635      * <ul>
1636      * <li> /storage/emulated/0/Screenshots or
1637      * <li> /storage/emulated/0/DCIM/Screenshots or
1638      * <li> /storage/emulated/0/Pictures/Screenshots ...
1639      * </ul>
1640      * Some examples of directories that can be considered as hidden with nomedia are
1641      * <ul>
1642      * <li> /storage/emulated/0/foo/Screenshots or
1643      * <li> /storage/emulated/0/DCIM/Foo/Screenshots or
1644      * <li> /storage/emulated/0/Pictures/foo/bar/Screenshots ...
1645      * </ul>
1646      */
1647     private static boolean isScreenshotsDirNonHidden(@NonNull String[] relativePath,
1648             @NonNull String name) {
1649         if (name.equalsIgnoreCase(Environment.DIRECTORY_SCREENSHOTS)) {
1650             return (relativePath.length == 1 &&
1651                 (TextUtils.isEmpty(relativePath[0]) || isDefaultDirectoryName(relativePath[0])));
1652         }
1653         return false;
1654     }
1655 
1656     /**
1657      * Test if this given file should be considered hidden.
1658      */
1659     @VisibleForTesting
1660     public static boolean isFileHidden(@NonNull File file) {
1661         final String name = file.getName();
1662 
1663         // Handle well-known file names that are pending or trashed; they
1664         // normally appear hidden, but we give them special treatment
1665         if (PATTERN_EXPIRES_FILE.matcher(name).matches()) {
1666             return false;
1667         }
1668 
1669         // Otherwise fall back to file name
1670         if (name.startsWith(".")) {
1671             return true;
1672         }
1673         return false;
1674     }
1675 
1676     /**
1677      * Clears all app's external cache directories, i.e. for each app we delete
1678      * /sdcard/Android/data/app/cache/* but we keep the directory itself.
1679      *
1680      * @return 0 in case of success, or {@link OsConstants#EIO} if any error occurs.
1681      *
1682      * <p>This method doesn't perform any checks, so make sure that the calling package is allowed
1683      * to clear cache directories first.
1684      *
1685      * <p>If this method returned {@link OsConstants#EIO}, then we can't guarantee whether all, none
1686      * or part of the directories were cleared.
1687      */
1688     public static int clearAppCacheDirectories() {
1689         int status = 0;
1690         Log.i(TAG, "Clearing cache for all apps");
1691         final File rootDataDir = buildPath(Environment.getExternalStorageDirectory(),
1692                 "Android", "data");
1693         File[] appDataDirs = rootDataDir.listFiles();
1694         if (appDataDirs == null) {
1695             // Couldn't delete any app cache dirs because the call to list files in root data dir
1696             // failed (b/234521806). It is not clear why this call would fail because root data
1697             // dir path should be well-formed.
1698             Log.e(TAG, String.format("Couldn't delete any app cache dirs in root data dir %s !",
1699                     rootDataDir.getAbsolutePath()));
1700             status = OsConstants.EIO;
1701         } else {
1702             for (File appDataDir : appDataDirs) {
1703                 try {
1704                     final File appCacheDir = new File(appDataDir, "cache");
1705                     if (appCacheDir.isDirectory()) {
1706                         FileUtils.deleteContents(appCacheDir);
1707                     }
1708                 } catch (Exception e) {
1709                     // We want to avoid crashing MediaProvider at all costs, so we handle all
1710                     // "generic" exceptions here, and just report to the caller that an IO exception
1711                     // has occurred. We still try to clear the rest of the directories.
1712                     Log.e(TAG, "Couldn't delete all app cache dirs!", e);
1713                     status = OsConstants.EIO;
1714                 }
1715             }
1716         }
1717         return status;
1718     }
1719 
1720     /**
1721      * @return {@code true} if {@code dir} has nomedia and it is dirty directory, so it should be
1722      * scanned. Returns {@code false} otherwise.
1723      */
1724     public static boolean isDirectoryDirty(File dir) {
1725         File nomedia = new File(dir, ".nomedia");
1726 
1727         // We return false for directories that don't have .nomedia
1728         if (!nomedia.exists()) {
1729             return false;
1730         }
1731 
1732         // We don't write to ".nomedia" dirs, only to ".nomedia" files. If this ".nomedia" is not
1733         // a file, then don't try to read it.
1734         if (!nomedia.isFile()) {
1735             return true;
1736         }
1737 
1738         try {
1739             Optional<String> expectedPath = readString(nomedia);
1740             // Returns true If .nomedia file is empty or content doesn't match |dir|
1741             // Returns false otherwise
1742             return !expectedPath.isPresent()
1743                     || !expectedPath.get().equalsIgnoreCase(dir.getPath());
1744         } catch (IOException e) {
1745             Log.w(TAG, "Failed to read directory dirty" + dir);
1746             return true;
1747         }
1748     }
1749 
1750     /**
1751      * {@code isDirty} == {@code true} will force {@code dir} scanning even if it's hidden
1752      * {@code isDirty} == {@code false} will skip {@code dir} scanning on next scan.
1753      */
1754     public static void setDirectoryDirty(File dir, boolean isDirty) {
1755         File nomedia = new File(dir, ".nomedia");
1756         if (nomedia.exists() && nomedia.isFile()) {
1757             try {
1758                 writeString(nomedia, isDirty ? Optional.of("") : Optional.of(dir.getPath()));
1759             } catch (IOException e) {
1760                 Log.w(TAG, "Failed to change directory dirty: " + dir + ". isDirty: " + isDirty);
1761             }
1762         }
1763     }
1764 
1765     /**
1766      * @return the folder containing the top-most .nomedia in {@code file} hierarchy.
1767      * E.g input as /sdcard/foo/bar/ will return /sdcard/foo
1768      * even if foo and bar contain .nomedia files.
1769      *
1770      * Returns {@code null} if there's no .nomedia in hierarchy
1771      */
1772     public static File getTopLevelNoMedia(@NonNull File file) {
1773         File topNoMediaDir = null;
1774 
1775         File parent = file;
1776         while (parent != null) {
1777             File nomedia = new File(parent, ".nomedia");
1778             if (nomedia.exists()) {
1779                 topNoMediaDir = parent;
1780             }
1781             parent = parent.getParentFile();
1782         }
1783 
1784         return topNoMediaDir;
1785     }
1786 
1787     /**
1788      * Generate the extended absolute path from the expired file path
1789      * E.g. the input expiredFilePath is /storage/emulated/0/DCIM/.trashed-1621147340-test.jpg
1790      * The returned result is /storage/emulated/0/DCIM/.trashed-1888888888-test.jpg
1791      *
1792      * @hide
1793      */
1794     @Nullable
1795     public static String getAbsoluteExtendedPath(@NonNull String expiredFilePath,
1796             long extendedTime) {
1797         final String displayName = extractDisplayName(expiredFilePath);
1798 
1799         final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(displayName);
1800         if (matcher.matches()) {
1801             final String newDisplayName = String.format(Locale.US, ".%s-%d-%s", matcher.group(1),
1802                     extendedTime, matcher.group(3));
1803             final int lastSlash = expiredFilePath.lastIndexOf('/');
1804             final String newPath = expiredFilePath.substring(0, lastSlash + 1).concat(
1805                     newDisplayName);
1806             return newPath;
1807         }
1808 
1809         return null;
1810     }
1811 
1812     public static File buildPrimaryVolumeFile(int userId, String... segments) {
1813         return buildPath(new File("/storage/emulated/" + userId), segments);
1814     }
1815 
1816     private static final String LOWER_FS_PREFIX = "/storage/";
1817     private static final String FUSE_FS_PREFIX = "/mnt/user/" + UserHandle.myUserId() + "/";
1818 
1819     public static File toFuseFile(File file) {
1820         return new File(file.getPath().replaceFirst(LOWER_FS_PREFIX, FUSE_FS_PREFIX));
1821     }
1822 
1823     public static File fromFuseFile(File file) {
1824         return new File(file.getPath().replaceFirst(FUSE_FS_PREFIX, LOWER_FS_PREFIX));
1825     }
1826 
1827     /**
1828      * Returns the canonical {@link File} for the provided abstract pathname.
1829      *
1830      * @return The canonical pathname string denoting the same file or directory as this abstract
1831      *         pathname
1832      * @see File#getCanonicalFile()
1833      */
1834     @NonNull
1835     public static File getCanonicalFile(@NonNull String path) throws IOException {
1836         Objects.requireNonNull(path);
1837         return new File(path).getCanonicalFile();
1838     }
1839 
1840     /**
1841      * Returns the canonical pathname string of the provided abstract pathname.
1842      *
1843      * @return The canonical pathname string denoting the same file or directory as this abstract
1844      *         pathname.
1845      * @see File#getCanonicalPath()
1846      */
1847     @NonNull
1848     public static String getCanonicalPath(@NonNull String path) throws IOException {
1849         Objects.requireNonNull(path);
1850         return new File(path).getCanonicalPath();
1851     }
1852 
1853     /**
1854      * A wrapper for {@link File#getCanonicalFile()} that catches {@link IOException}-s and
1855      * re-throws them as {@link RuntimeException}-s.
1856      *
1857      * @see File#getCanonicalFile()
1858      */
1859     @NonNull
1860     public static File canonicalize(@NonNull File file) throws IOException {
1861         Objects.requireNonNull(file);
1862         return file.getCanonicalFile();
1863     }
1864 }
1865