• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import static com.android.providers.media.util.LegacyLogging.TAG;
20 
21 import android.content.ContentValues;
22 import android.os.Environment;
23 import android.os.SystemProperties;
24 import android.provider.MediaStore;
25 import android.provider.MediaStore.Audio.AudioColumns;
26 import android.provider.MediaStore.MediaColumns;
27 import android.text.TextUtils;
28 import android.util.ArrayMap;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.modules.utils.build.SdkLevel;
36 
37 import java.io.File;
38 import java.io.IOException;
39 import java.nio.file.FileVisitResult;
40 import java.nio.file.FileVisitor;
41 import java.nio.file.Files;
42 import java.nio.file.Path;
43 import java.nio.file.attribute.BasicFileAttributes;
44 import java.util.Locale;
45 import java.util.Objects;
46 import java.util.function.Consumer;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49 
50 public class LegacyFileUtils {
51 
52     /**
53      * Recursively walk the contents of the given {@link Path}, invoking the
54      * given {@link Consumer} for every file and directory encountered. This is
55      * typically used for recursively deleting a directory tree.
56      * <p>
57      * Gracefully attempts to process as much as possible in the face of any
58      * failures.
59      */
walkFileTreeContents(@onNull Path path, @NonNull Consumer<Path> operation)60     public static void walkFileTreeContents(@NonNull Path path, @NonNull Consumer<Path> operation) {
61         try {
62             Files.walkFileTree(path, new FileVisitor<Path>() {
63                 @Override
64                 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
65                     return FileVisitResult.CONTINUE;
66                 }
67 
68                 @Override
69                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
70                     if (!Objects.equals(path, file)) {
71                         operation.accept(file);
72                     }
73                     return FileVisitResult.CONTINUE;
74                 }
75 
76                 @Override
77                 public FileVisitResult visitFileFailed(Path file, IOException e) {
78                     Log.w(TAG, "Failed to visit " + file, e);
79                     return FileVisitResult.CONTINUE;
80                 }
81 
82                 @Override
83                 public FileVisitResult postVisitDirectory(Path dir, IOException e) {
84                     if (!Objects.equals(path, dir)) {
85                         operation.accept(dir);
86                     }
87                     return FileVisitResult.CONTINUE;
88                 }
89             });
90         } catch (IOException e) {
91             Log.w(TAG, "Failed to walk " + path, e);
92         }
93     }
94 
95     /**
96      * Recursively delete all contents inside the given directory. Gracefully
97      * attempts to delete as much as possible in the face of any failures.
98      *
99      * @deprecated if you're calling this from inside {@code MediaProvider}, you
100      * likely want to call {@link #forEach} with a separate
101      * invocation to invalidate FUSE entries.
102      */
103     @Deprecated
deleteContents(@onNull File dir)104     public static void deleteContents(@NonNull File dir) {
105         walkFileTreeContents(dir.toPath(), (path) -> {
106             path.toFile().delete();
107         });
108     }
109 
extractDisplayName(@ullable String data)110     public static @Nullable String extractDisplayName(@Nullable String data) {
111         if (data == null) return null;
112         if (data.indexOf('/') == -1) {
113             return data;
114         }
115         if (data.endsWith("/")) {
116             data = data.substring(0, data.length() - 1);
117         }
118         return data.substring(data.lastIndexOf('/') + 1);
119     }
120 
extractFileExtension(@ullable String data)121     public static @Nullable String extractFileExtension(@Nullable String data) {
122         if (data == null) return null;
123         data = extractDisplayName(data);
124 
125         final int lastDot = data.lastIndexOf('.');
126         if (lastDot == -1) {
127             return null;
128         } else {
129             return data.substring(lastDot + 1);
130         }
131     }
132 
133     public static final Pattern PATTERN_DOWNLOADS_FILE = Pattern.compile(
134             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Download/.+");
135     public static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
136             "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$");
137 
138     /**
139      * File prefix indicating that the file {@link MediaColumns#IS_PENDING}.
140      */
141     public static final String PREFIX_PENDING = "pending";
142 
143     /**
144      * File prefix indicating that the file {@link MediaColumns#IS_TRASHED}.
145      */
146     public static final String PREFIX_TRASHED = "trashed";
147 
148     private static final boolean PROP_CROSS_USER_ALLOWED =
149             SystemProperties.getBoolean("external_storage.cross_user.enabled", false);
150 
151     private static final String PROP_CROSS_USER_ROOT = isCrossUserEnabled()
152             ? SystemProperties.get("external_storage.cross_user.root", "") : "";
153 
154     private static final String PROP_CROSS_USER_ROOT_PATTERN = ((PROP_CROSS_USER_ROOT.isEmpty())
155             ? "" : "(?:" + PROP_CROSS_USER_ROOT + "/)?");
156 
157     /**
158      * Regex that matches paths in all well-known package-specific directories,
159      * and which captures the package name as the first group.
160      */
161     public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
162             "(?i)^/storage/[^/]+/(?:[0-9]+/)?"
163                     + PROP_CROSS_USER_ROOT_PATTERN
164                     + "Android/(?:data|media|obb)/([^/]+)(/?.*)?");
165 
166     /**
167      * The recordings directory. This is used for R OS. For S OS or later,
168      * we use {@link Environment#DIRECTORY_RECORDINGS} directly.
169      */
170     public static final String DIRECTORY_RECORDINGS = "Recordings";
171 
172     /**
173      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}
174      */
175     private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
176             "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)");
177 
178     /**
179      * Regex that matches paths under well-known storage paths.
180      */
181     private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
182             "(?i)^/storage/([^/]+)");
183 
isCrossUserEnabled()184     public static boolean isCrossUserEnabled() {
185         return PROP_CROSS_USER_ALLOWED || SdkLevel.isAtLeastS();
186     }
187 
normalizeUuid(@ullable String fsUuid)188     private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
189         return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
190     }
191 
extractVolumeName(@ullable String data)192     public static @Nullable String extractVolumeName(@Nullable String data) {
193         if (data == null) return null;
194         final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
195         if (matcher.find()) {
196             final String volumeName = matcher.group(1);
197             if (volumeName.equals("emulated")) {
198                 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
199             } else {
200                 return normalizeUuid(volumeName);
201             }
202         } else {
203             return MediaStore.VOLUME_INTERNAL;
204         }
205     }
206 
extractRelativePath(@ullable String data)207     public static @Nullable String extractRelativePath(@Nullable String data) {
208         if (data == null) return null;
209 
210         final String path;
211         try {
212             path = getCanonicalPath(data);
213         } catch (IOException e) {
214             Log.d(TAG, "Unable to get canonical path from invalid data path: " + data, e);
215             return null;
216         }
217 
218         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(path);
219         if (matcher.find()) {
220             final int lastSlash = path.lastIndexOf('/');
221             if (lastSlash == -1 || lastSlash < matcher.end()) {
222                 // This is a file in the top-level directory, so relative path is "/"
223                 // which is different than null, which means unknown path
224                 return "/";
225             } else {
226                 return path.substring(matcher.end(), lastSlash + 1);
227             }
228         } else {
229             return null;
230         }
231     }
232 
233     /**
234      * Compute several scattered {@link MediaColumns} values from
235      * {@link MediaColumns#DATA}. This method performs no enforcement of
236      * argument validity.
237      */
computeValuesFromData(@onNull ContentValues values, boolean isForFuse)238     public static void computeValuesFromData(@NonNull ContentValues values, boolean isForFuse) {
239         // Worst case we have to assume no bucket details
240         values.remove(MediaColumns.VOLUME_NAME);
241         values.remove(MediaColumns.RELATIVE_PATH);
242         values.remove(MediaColumns.IS_TRASHED);
243         values.remove(MediaColumns.DATE_EXPIRES);
244         values.remove(MediaColumns.DISPLAY_NAME);
245         values.remove(MediaColumns.BUCKET_ID);
246         values.remove(MediaColumns.BUCKET_DISPLAY_NAME);
247 
248         String data = values.getAsString(MediaColumns.DATA);
249         if (TextUtils.isEmpty(data)) return;
250 
251         try {
252             data = new File(data).getCanonicalPath();
253             values.put(MediaColumns.DATA, data);
254         } catch (IOException e) {
255             throw new IllegalArgumentException(
256                     String.format(Locale.ROOT, "Invalid file path:%s in request.", data));
257         }
258 
259         final File file = new File(data);
260         final File fileLower = new File(data.toLowerCase(Locale.ROOT));
261 
262         values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
263         values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
264         final String displayName = extractDisplayName(data);
265         final Matcher matcher = LegacyFileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
266         if (matcher.matches()) {
267             values.put(MediaColumns.IS_PENDING,
268                     matcher.group(1).equals(LegacyFileUtils.PREFIX_PENDING) ? 1 : 0);
269             values.put(MediaColumns.IS_TRASHED,
270                     matcher.group(1).equals(LegacyFileUtils.PREFIX_TRASHED) ? 1 : 0);
271             values.put(MediaColumns.DATE_EXPIRES, Long.parseLong(matcher.group(2)));
272             values.put(MediaColumns.DISPLAY_NAME, matcher.group(3));
273         } else {
274             if (isForFuse) {
275                 // Allow Fuse thread to set IS_PENDING when using DATA column.
276                 // TODO(b/156867379) Unset IS_PENDING when Fuse thread doesn't explicitly specify
277                 // IS_PENDING. It can't be done now because we scan after create. Scan doesn't
278                 // explicitly specify the value of IS_PENDING.
279             } else {
280                 values.put(MediaColumns.IS_PENDING, 0);
281             }
282             values.put(MediaColumns.IS_TRASHED, 0);
283             values.putNull(MediaColumns.DATE_EXPIRES);
284             values.put(MediaColumns.DISPLAY_NAME, displayName);
285         }
286 
287         // Buckets are the parent directory
288         final String parent = fileLower.getParent();
289         if (parent != null) {
290             values.put(MediaColumns.BUCKET_ID, parent.hashCode());
291             // The relative path for files in the top directory is "/"
292             if (!"/".equals(values.getAsString(MediaColumns.RELATIVE_PATH))) {
293                 values.put(MediaColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName());
294             } else {
295                 values.putNull(MediaColumns.BUCKET_DISPLAY_NAME);
296             }
297         }
298     }
299 
300     @VisibleForTesting
301     static ArrayMap<String, String> sAudioTypes = new ArrayMap<>();
302 
303     static {
sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE)304         sAudioTypes.put(Environment.DIRECTORY_RINGTONES, AudioColumns.IS_RINGTONE);
sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION)305         sAudioTypes.put(Environment.DIRECTORY_NOTIFICATIONS, AudioColumns.IS_NOTIFICATION);
sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM)306         sAudioTypes.put(Environment.DIRECTORY_ALARMS, AudioColumns.IS_ALARM);
sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST)307         sAudioTypes.put(Environment.DIRECTORY_PODCASTS, AudioColumns.IS_PODCAST);
sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK)308         sAudioTypes.put(Environment.DIRECTORY_AUDIOBOOKS, AudioColumns.IS_AUDIOBOOK);
sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC)309         sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
310         if (SdkLevel.isAtLeastS()) {
sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING)311             sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
312         } else {
sAudioTypes.put(LegacyFileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING)313             sAudioTypes.put(LegacyFileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
314         }
315     }
316 
317     /**
318      * Returns the canonical pathname string of the provided abstract pathname.
319      *
320      * @return The canonical pathname string denoting the same file or directory as this abstract
321      * pathname.
322      * @see File#getCanonicalPath()
323      */
324     @NonNull
getCanonicalPath(@onNull String path)325     public static String getCanonicalPath(@NonNull String path) throws IOException {
326         Objects.requireNonNull(path);
327         return new File(path).getCanonicalPath();
328     }
329 
330 }
331