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