1 /*
2  * Copyright (C) 2008 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.downloads;
18 
19 import static android.os.Environment.buildExternalStorageAndroidObbDirs;
20 import static android.os.Environment.buildExternalStorageAppDataDirs;
21 import static android.os.Environment.buildExternalStorageAppMediaDirs;
22 import static android.os.Environment.buildExternalStorageAppObbDirs;
23 import static android.os.Environment.buildExternalStoragePublicDirs;
24 import static android.os.Process.INVALID_UID;
25 import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
26 import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
27 import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
28 import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
29 import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING;
30 import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
31 import static android.provider.Downloads.Impl._DATA;
32 
33 import static com.android.providers.downloads.Constants.TAG;
34 
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.app.job.JobInfo;
38 import android.app.job.JobScheduler;
39 import android.content.ComponentName;
40 import android.content.ContentProvider;
41 import android.content.ContentResolver;
42 import android.content.ContentValues;
43 import android.content.Context;
44 import android.database.Cursor;
45 import android.net.Uri;
46 import android.os.Environment;
47 import android.os.FileUtils;
48 import android.os.Handler;
49 import android.os.HandlerThread;
50 import android.os.Process;
51 import android.os.SystemClock;
52 import android.os.UserHandle;
53 import android.os.storage.StorageManager;
54 import android.os.storage.StorageVolume;
55 import android.provider.Downloads;
56 import android.provider.MediaStore;
57 import android.text.TextUtils;
58 import android.util.Log;
59 import android.util.SparseArray;
60 import android.webkit.MimeTypeMap;
61 
62 import com.android.internal.annotations.VisibleForTesting;
63 import com.android.internal.util.ArrayUtils;
64 
65 import java.io.File;
66 import java.io.IOException;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Locale;
70 import java.util.Random;
71 import java.util.regex.Matcher;
72 import java.util.regex.Pattern;
73 
74 /**
75  * Some helper functions for the download manager
76  */
77 public class Helpers {
78     public static Random sRandom = new Random(SystemClock.uptimeMillis());
79 
80     /** Regex used to parse content-disposition headers */
81     private static final Pattern CONTENT_DISPOSITION_PATTERN =
82             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
83 
84     private static final Pattern PATTERN_ANDROID_DIRS =
85             Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(?:data|obb|media)/.+");
86 
87     private static final Pattern PATTERN_PUBLIC_DIRS =
88             Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/([^/]+)/.+");
89 
90     private static final Object sUniqueLock = new Object();
91 
92     private static HandlerThread sAsyncHandlerThread;
93     private static Handler sAsyncHandler;
94 
95     private static SystemFacade sSystemFacade;
96     private static DownloadNotifier sNotifier;
97 
Helpers()98     private Helpers() {
99     }
100 
getAsyncHandler()101     public synchronized static Handler getAsyncHandler() {
102         if (sAsyncHandlerThread == null) {
103             sAsyncHandlerThread = new HandlerThread("sAsyncHandlerThread",
104                     Process.THREAD_PRIORITY_BACKGROUND);
105             sAsyncHandlerThread.start();
106             sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper());
107         }
108         return sAsyncHandler;
109     }
110 
111     @VisibleForTesting
setSystemFacade(SystemFacade systemFacade)112     public synchronized static void setSystemFacade(SystemFacade systemFacade) {
113         sSystemFacade = systemFacade;
114     }
115 
getSystemFacade(Context context)116     public synchronized static SystemFacade getSystemFacade(Context context) {
117         if (sSystemFacade == null) {
118             sSystemFacade = new RealSystemFacade(context);
119         }
120         return sSystemFacade;
121     }
122 
getDownloadNotifier(Context context)123     public synchronized static DownloadNotifier getDownloadNotifier(Context context) {
124         if (sNotifier == null) {
125             sNotifier = new DownloadNotifier(context);
126         }
127         return sNotifier;
128     }
129 
getString(Cursor cursor, String col)130     public static String getString(Cursor cursor, String col) {
131         return cursor.getString(cursor.getColumnIndexOrThrow(col));
132     }
133 
getInt(Cursor cursor, String col)134     public static int getInt(Cursor cursor, String col) {
135         return cursor.getInt(cursor.getColumnIndexOrThrow(col));
136     }
137 
scheduleJob(Context context, long downloadId)138     public static void scheduleJob(Context context, long downloadId) {
139         final boolean scheduled = scheduleJob(context,
140                 DownloadInfo.queryDownloadInfo(context, downloadId));
141         if (!scheduled) {
142             // If we didn't schedule a future job, kick off a notification
143             // update pass immediately
144             getDownloadNotifier(context).update();
145         }
146     }
147 
148     /**
149      * Schedule (or reschedule) a job for the given {@link DownloadInfo} using
150      * its current state to define job constraints.
151      */
scheduleJob(Context context, DownloadInfo info)152     public static boolean scheduleJob(Context context, DownloadInfo info) {
153         if (info == null) return false;
154 
155         final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
156 
157         // Tear down any existing job for this download
158         final int jobId = (int) info.mId;
159         scheduler.cancel(jobId);
160 
161         // Skip scheduling if download is paused or finished
162         if (!info.isReadyToSchedule()) return false;
163 
164         final JobInfo.Builder builder = new JobInfo.Builder(jobId,
165                 new ComponentName(context, DownloadJobService.class));
166 
167         // When this download will show a notification, run with a higher
168         // priority, since it's effectively a foreground service
169         if (info.isVisible()) {
170             builder.setPriority(JobInfo.PRIORITY_FOREGROUND_SERVICE);
171             builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND);
172         }
173 
174         // We might have a backoff constraint due to errors
175         final long latency = info.getMinimumLatency();
176         if (latency > 0) {
177             builder.setMinimumLatency(latency);
178         }
179 
180         // We always require a network, but the type of network might be further
181         // restricted based on download request or user override
182         builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes));
183 
184         if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) {
185             builder.setRequiresCharging(true);
186         }
187         if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) {
188             builder.setRequiresDeviceIdle(true);
189         }
190 
191         // Provide estimated network size, when possible
192         if (info.mTotalBytes > 0) {
193             if (info.mCurrentBytes > 0 && !TextUtils.isEmpty(info.mETag)) {
194                 // If we're resuming an in-progress download, we only need to
195                 // download the remaining bytes.
196                 builder.setEstimatedNetworkBytes(info.mTotalBytes - info.mCurrentBytes,
197                         JobInfo.NETWORK_BYTES_UNKNOWN);
198             } else {
199                 builder.setEstimatedNetworkBytes(info.mTotalBytes, JobInfo.NETWORK_BYTES_UNKNOWN);
200             }
201         }
202 
203         // If package name was filtered during insert (probably due to being
204         // invalid), blame based on the requesting UID instead
205         String packageName = info.mPackage;
206         if (packageName == null) {
207             packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0];
208         }
209 
210         scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG);
211         return true;
212     }
213 
214     /*
215      * Parse the Content-Disposition HTTP Header. The format of the header
216      * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
217      * This header provides a filename for content that is going to be
218      * downloaded to the file system. We only support the attachment type.
219      */
parseContentDisposition(String contentDisposition)220     private static String parseContentDisposition(String contentDisposition) {
221         try {
222             Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
223             if (m.find()) {
224                 return m.group(1);
225             }
226         } catch (IllegalStateException ex) {
227              // This function is defined as returning null when it can't parse the header
228         }
229         return null;
230     }
231 
232     /**
233      * Creates a filename (where the file should be saved) from info about a download.
234      * This file will be touched to reserve it.
235      */
generateSaveFile(Context context, String url, String hint, String contentDisposition, String contentLocation, String mimeType, int destination)236     static String generateSaveFile(Context context, String url, String hint,
237             String contentDisposition, String contentLocation, String mimeType, int destination)
238             throws IOException {
239 
240         final File parent;
241         final File[] parentTest;
242         String name = null;
243 
244         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
245             final File file = new File(Uri.parse(hint).getPath());
246             parent = file.getParentFile().getAbsoluteFile();
247             parentTest = new File[] { parent };
248             name = file.getName();
249         } else {
250             parent = getRunningDestinationDirectory(context, destination);
251             parentTest = new File[] {
252                     parent,
253                     getSuccessDestinationDirectory(context, destination)
254             };
255             name = chooseFilename(url, hint, contentDisposition, contentLocation);
256         }
257 
258         // Ensure target directories are ready
259         for (File test : parentTest) {
260             if (!(test.isDirectory() || test.mkdirs())) {
261                 throw new IOException("Failed to create parent for " + test);
262             }
263         }
264 
265         if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
266             name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name);
267         }
268 
269         final String prefix;
270         final String suffix;
271         final int dotIndex = name.lastIndexOf('.');
272         final boolean missingExtension = dotIndex < 0;
273         if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
274             // Destination is explicitly set - do not change the extension
275             if (missingExtension) {
276                 prefix = name;
277                 suffix = "";
278             } else {
279                 prefix = name.substring(0, dotIndex);
280                 suffix = name.substring(dotIndex);
281             }
282         } else {
283             // Split filename between base and extension
284             // Add an extension if filename does not have one
285             if (missingExtension) {
286                 prefix = name;
287                 suffix = chooseExtensionFromMimeType(mimeType, true);
288             } else {
289                 prefix = name.substring(0, dotIndex);
290                 suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex);
291             }
292         }
293 
294         synchronized (sUniqueLock) {
295             name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
296 
297             // Claim this filename inside lock to prevent other threads from
298             // clobbering us. We're not paranoid enough to use O_EXCL.
299             final File file = new File(parent, name);
300             file.createNewFile();
301             return file.getAbsolutePath();
302         }
303     }
304 
305     private static String chooseFilename(String url, String hint, String contentDisposition,
306             String contentLocation) {
307         String filename = null;
308 
309         // First, try to use the hint from the application, if there's one
310         if (filename == null && hint != null && !hint.endsWith("/")) {
311             if (Constants.LOGVV) {
312                 Log.v(Constants.TAG, "getting filename from hint");
313             }
314             int index = hint.lastIndexOf('/') + 1;
315             if (index > 0) {
316                 filename = hint.substring(index);
317             } else {
318                 filename = hint;
319             }
320         }
321 
322         // If we couldn't do anything with the hint, move toward the content disposition
323         if (filename == null && contentDisposition != null) {
324             filename = parseContentDisposition(contentDisposition);
325             if (filename != null) {
326                 if (Constants.LOGVV) {
327                     Log.v(Constants.TAG, "getting filename from content-disposition");
328                 }
329                 int index = filename.lastIndexOf('/') + 1;
330                 if (index > 0) {
331                     filename = filename.substring(index);
332                 }
333             }
334         }
335 
336         // If we still have nothing at this point, try the content location
337         if (filename == null && contentLocation != null) {
338             String decodedContentLocation = Uri.decode(contentLocation);
339             if (decodedContentLocation != null
340                     && !decodedContentLocation.endsWith("/")
341                     && decodedContentLocation.indexOf('?') < 0) {
342                 if (Constants.LOGVV) {
343                     Log.v(Constants.TAG, "getting filename from content-location");
344                 }
345                 int index = decodedContentLocation.lastIndexOf('/') + 1;
346                 if (index > 0) {
347                     filename = decodedContentLocation.substring(index);
348                 } else {
349                     filename = decodedContentLocation;
350                 }
351             }
352         }
353 
354         // If all the other http-related approaches failed, use the plain uri
355         if (filename == null) {
356             String decodedUrl = Uri.decode(url);
357             if (decodedUrl != null
358                     && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) {
359                 int index = decodedUrl.lastIndexOf('/') + 1;
360                 if (index > 0) {
361                     if (Constants.LOGVV) {
362                         Log.v(Constants.TAG, "getting filename from uri");
363                     }
364                     filename = decodedUrl.substring(index);
365                 }
366             }
367         }
368 
369         // Finally, if couldn't get filename from URI, get a generic filename
370         if (filename == null) {
371             if (Constants.LOGVV) {
372                 Log.v(Constants.TAG, "using default filename");
373             }
374             filename = Constants.DEFAULT_DL_FILENAME;
375         }
376 
377         // The VFAT file system is assumed as target for downloads.
378         // Replace invalid characters according to the specifications of VFAT.
379         filename = FileUtils.buildValidFatFilename(filename);
380 
381         return filename;
382     }
383 
chooseExtensionFromMimeType(String mimeType, boolean useDefaults)384     private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) {
385         String extension = null;
386         if (mimeType != null) {
387             extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
388             if (extension != null) {
389                 if (Constants.LOGVV) {
390                     Log.v(Constants.TAG, "adding extension from type");
391                 }
392                 extension = "." + extension;
393             } else {
394                 if (Constants.LOGVV) {
395                     Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
396                 }
397             }
398         }
399         if (extension == null) {
400             if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
401                 if (mimeType.equalsIgnoreCase("text/html")) {
402                     if (Constants.LOGVV) {
403                         Log.v(Constants.TAG, "adding default html extension");
404                     }
405                     extension = Constants.DEFAULT_DL_HTML_EXTENSION;
406                 } else if (useDefaults) {
407                     if (Constants.LOGVV) {
408                         Log.v(Constants.TAG, "adding default text extension");
409                     }
410                     extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
411                 }
412             } else if (useDefaults) {
413                 if (Constants.LOGVV) {
414                     Log.v(Constants.TAG, "adding default binary extension");
415                 }
416                 extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
417             }
418         }
419         return extension;
420     }
421 
chooseExtensionFromFilename(String mimeType, int destination, String filename, int lastDotIndex)422     private static String chooseExtensionFromFilename(String mimeType, int destination,
423             String filename, int lastDotIndex) {
424         String extension = null;
425         if (mimeType != null) {
426             // Compare the last segment of the extension against the mime type.
427             // If there's a mismatch, discard the entire extension.
428             String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
429                     filename.substring(lastDotIndex + 1));
430             if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
431                 extension = chooseExtensionFromMimeType(mimeType, false);
432                 if (extension != null) {
433                     if (Constants.LOGVV) {
434                         Log.v(Constants.TAG, "substituting extension from type");
435                     }
436                 } else {
437                     if (Constants.LOGVV) {
438                         Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
439                     }
440                 }
441             }
442         }
443         if (extension == null) {
444             if (Constants.LOGVV) {
445                 Log.v(Constants.TAG, "keeping extension");
446             }
447             extension = filename.substring(lastDotIndex);
448         }
449         return extension;
450     }
451 
isFilenameAvailableLocked(File[] parents, String name)452     private static boolean isFilenameAvailableLocked(File[] parents, String name) {
453         if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false;
454 
455         for (File parent : parents) {
456             if (new File(parent, name).exists()) {
457                 return false;
458             }
459         }
460 
461         return true;
462     }
463 
generateAvailableFilenameLocked( File[] parents, String prefix, String suffix)464     private static String generateAvailableFilenameLocked(
465             File[] parents, String prefix, String suffix) throws IOException {
466         String name = prefix + suffix;
467         if (isFilenameAvailableLocked(parents, name)) {
468             return name;
469         }
470 
471         /*
472         * This number is used to generate partially randomized filenames to avoid
473         * collisions.
474         * It starts at 1.
475         * The next 9 iterations increment it by 1 at a time (up to 10).
476         * The next 9 iterations increment it by 1 to 10 (random) at a time.
477         * The next 9 iterations increment it by 1 to 100 (random) at a time.
478         * ... Up to the point where it increases by 100000000 at a time.
479         * (the maximum value that can be reached is 1000000000)
480         * As soon as a number is reached that generates a filename that doesn't exist,
481         *     that filename is used.
482         * If the filename coming in is [base].[ext], the generated filenames are
483         *     [base]-[sequence].[ext].
484         */
485         int sequence = 1;
486         for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
487             for (int iteration = 0; iteration < 9; ++iteration) {
488                 name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix;
489                 if (isFilenameAvailableLocked(parents, name)) {
490                     return name;
491                 }
492                 sequence += sRandom.nextInt(magnitude) + 1;
493             }
494         }
495 
496         throw new IOException("Failed to generate an available filename");
497     }
498 
convertToMediaStoreDownloadsUri(Uri mediaStoreUri)499     public static Uri convertToMediaStoreDownloadsUri(Uri mediaStoreUri) {
500         final String volumeName = MediaStore.getVolumeName(mediaStoreUri);
501         final long id = android.content.ContentUris.parseId(mediaStoreUri);
502         return MediaStore.Downloads.getContentUri(volumeName, id);
503     }
504 
triggerMediaScan(android.content.ContentProviderClient mediaProviderClient, File file)505     public static Uri triggerMediaScan(android.content.ContentProviderClient mediaProviderClient,
506             File file) {
507         return MediaStore.scanFile(ContentResolver.wrap(mediaProviderClient), file);
508     }
509 
getContentUriForPath(Context context, String path)510     public static final Uri getContentUriForPath(Context context, String path) {
511         final StorageManager sm = context.getSystemService(StorageManager.class);
512         final String volumeName = sm.getStorageVolume(new File(path)).getMediaStoreVolumeName();
513         return MediaStore.Downloads.getContentUri(volumeName);
514     }
515 
isFileInExternalAndroidDirs(String filePath)516     public static boolean isFileInExternalAndroidDirs(String filePath) {
517         return PATTERN_ANDROID_DIRS.matcher(filePath).matches();
518     }
519 
isFilenameValid(Context context, File file)520     static boolean isFilenameValid(Context context, File file) {
521         return isFilenameValid(context, file, true);
522     }
523 
isFilenameValidInExternal(Context context, File file)524     static boolean isFilenameValidInExternal(Context context, File file) {
525         return isFilenameValid(context, file, false);
526     }
527 
528     /**
529      * Test if given file exists in one of the package-specific external storage
530      * directories that are always writable to apps, regardless of storage
531      * permission.
532      */
isFilenameValidInExternalPackage(Context context, File file, String packageName)533     static boolean isFilenameValidInExternalPackage(Context context, File file,
534             String packageName) {
535         try {
536             if (containsCanonical(buildExternalStorageAppDataDirs(packageName), file) ||
537                     containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
538                     containsCanonical(buildExternalStorageAppMediaDirs(packageName), file)) {
539                 return true;
540             }
541         } catch (IOException e) {
542             Log.w(TAG, "Failed to resolve canonical path: " + e);
543             return false;
544         }
545 
546         return false;
547     }
548 
isFilenameValidInExternalObbDir(File file)549     static boolean isFilenameValidInExternalObbDir(File file) {
550         try {
551             if (containsCanonical(buildExternalStorageAndroidObbDirs(), file)) {
552                 return true;
553             }
554         } catch (IOException e) {
555             Log.w(TAG, "Failed to resolve canonical path: " + e);
556             return false;
557         }
558 
559         return false;
560     }
561 
isFilenameValidInPublicDownloadsDir(File file)562     static boolean isFilenameValidInPublicDownloadsDir(File file) {
563         try {
564             if (containsCanonical(buildExternalStoragePublicDirs(
565                     Environment.DIRECTORY_DOWNLOADS), file)) {
566                 return true;
567             }
568         } catch (IOException e) {
569             Log.w(TAG, "Failed to resolve canonical path: " + e);
570             return false;
571         }
572 
573         return false;
574     }
575 
576     @com.android.internal.annotations.VisibleForTesting
isFilenameValidInKnownPublicDir(@ullable String filePath)577     public static boolean isFilenameValidInKnownPublicDir(@Nullable String filePath) {
578         if (filePath == null) {
579             return false;
580         }
581         final Matcher matcher = PATTERN_PUBLIC_DIRS.matcher(filePath);
582         if (matcher.matches()) {
583             final String publicDir = matcher.group(1);
584             return ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, publicDir);
585         }
586         return false;
587     }
588 
589     /**
590      * Checks whether the filename looks legitimate for security purposes. This
591      * prevents us from opening files that aren't actually downloads.
592      */
isFilenameValid(Context context, File file, boolean allowInternal)593     static boolean isFilenameValid(Context context, File file, boolean allowInternal) {
594         try {
595             if (allowInternal) {
596                 if (containsCanonical(context.getFilesDir(), file)
597                         || containsCanonical(context.getCacheDir(), file)
598                         || containsCanonical(Environment.getDownloadCacheDirectory(), file)) {
599                     return true;
600                 }
601             }
602 
603             final StorageVolume[] volumes = StorageManager.getVolumeList(UserHandle.myUserId(),
604                     StorageManager.FLAG_FOR_WRITE);
605             for (StorageVolume volume : volumes) {
606                 if (containsCanonical(volume.getPathFile(), file)) {
607                     return true;
608                 }
609             }
610         } catch (IOException e) {
611             Log.w(TAG, "Failed to resolve canonical path: " + e);
612             return false;
613         }
614 
615         return false;
616     }
617 
618     /**
619      * Shamelessly borrowed from
620      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
621      */
622     private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
623             "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?");
624 
625     /**
626      * Shamelessly borrowed from
627      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
628      */
629     private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
630             "(?i)^/storage/([^/]+)");
631 
632     /**
633      * Shamelessly borrowed from
634      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
635      */
normalizeUuid(@ullable String fsUuid)636     private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
637         return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
638     }
639 
640     /**
641      * Shamelessly borrowed from
642      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
643      */
extractVolumeName(@ullable String data)644     public static @Nullable String extractVolumeName(@Nullable String data) {
645         if (data == null) return null;
646         final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
647         if (matcher.find()) {
648             final String volumeName = matcher.group(1);
649             if (volumeName.equals("emulated")) {
650                 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
651             } else {
652                 return normalizeUuid(volumeName);
653             }
654         } else {
655             return MediaStore.VOLUME_INTERNAL;
656         }
657     }
658 
659     /**
660      * Shamelessly borrowed from
661      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
662      */
extractRelativePath(@ullable String data)663     public static @Nullable String extractRelativePath(@Nullable String data) {
664         if (data == null) return null;
665         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
666         if (matcher.find()) {
667             final int lastSlash = data.lastIndexOf('/');
668             if (lastSlash == -1 || lastSlash < matcher.end()) {
669                 // This is a file in the top-level directory, so relative path is "/"
670                 // which is different than null, which means unknown path
671                 return "/";
672             } else {
673                 return data.substring(matcher.end(), lastSlash + 1);
674             }
675         } else {
676             return null;
677         }
678     }
679 
680     /**
681      * Shamelessly borrowed from
682      * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
683      */
extractDisplayName(@ullable String data)684     public static @Nullable String extractDisplayName(@Nullable String data) {
685         if (data == null) return null;
686         if (data.indexOf('/') == -1) {
687             return data;
688         }
689         if (data.endsWith("/")) {
690             data = data.substring(0, data.length() - 1);
691         }
692         return data.substring(data.lastIndexOf('/') + 1);
693     }
694 
containsCanonical(File dir, File file)695     private static boolean containsCanonical(File dir, File file) throws IOException {
696         return FileUtils.contains(dir.getCanonicalFile(), file);
697     }
698 
containsCanonical(File[] dirs, File file)699     private static boolean containsCanonical(File[] dirs, File file) throws IOException {
700         for (File dir : dirs) {
701             if (containsCanonical(dir, file)) {
702                 return true;
703             }
704         }
705         return false;
706     }
707 
getRunningDestinationDirectory(Context context, int destination)708     public static File getRunningDestinationDirectory(Context context, int destination)
709             throws IOException {
710         return getDestinationDirectory(context, destination, true);
711     }
712 
getSuccessDestinationDirectory(Context context, int destination)713     public static File getSuccessDestinationDirectory(Context context, int destination)
714             throws IOException {
715         return getDestinationDirectory(context, destination, false);
716     }
717 
getDestinationDirectory(Context context, int destination, boolean running)718     private static File getDestinationDirectory(Context context, int destination, boolean running)
719             throws IOException {
720         switch (destination) {
721             case Downloads.Impl.DESTINATION_CACHE_PARTITION:
722             case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
723             case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
724                 if (running) {
725                     return context.getFilesDir();
726                 } else {
727                     return context.getCacheDir();
728                 }
729 
730             case Downloads.Impl.DESTINATION_EXTERNAL:
731                 final File target = new File(
732                         Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
733                 if (!target.isDirectory() && target.mkdirs()) {
734                     throw new IOException("unable to create external downloads directory");
735                 }
736                 return target;
737 
738             default:
739                 throw new IllegalStateException("unexpected destination: " + destination);
740         }
741     }
742 
743     @VisibleForTesting
handleRemovedUidEntries(@onNull Context context, ContentProvider downloadProvider, int removedUid)744     public static void handleRemovedUidEntries(@NonNull Context context,
745             ContentProvider downloadProvider, int removedUid) {
746         final SparseArray<String> knownUids = new SparseArray<>();
747         final ArrayList<Long> idsToDelete = new ArrayList<>();
748         final ArrayList<Long> idsToOrphan = new ArrayList<>();
749         final String selection = removedUid == INVALID_UID ? Constants.UID + " IS NOT NULL"
750                 : Constants.UID + "=" + removedUid;
751         try (Cursor cursor = downloadProvider.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
752                 new String[] { Downloads.Impl._ID, Constants.UID, COLUMN_DESTINATION, _DATA },
753                 selection, null, null)) {
754             while (cursor.moveToNext()) {
755                 final long downloadId = cursor.getLong(0);
756                 final int uid = cursor.getInt(1);
757 
758                 final String ownerPackageName;
759                 final int index = knownUids.indexOfKey(uid);
760                 if (index >= 0) {
761                     ownerPackageName = knownUids.valueAt(index);
762                 } else {
763                     ownerPackageName = getPackageForUid(context, uid);
764                     knownUids.put(uid, ownerPackageName);
765                 }
766 
767                 if (ownerPackageName == null) {
768                     final int destination = cursor.getInt(2);
769                     final String filePath = cursor.getString(3);
770 
771                     if ((destination == DESTINATION_EXTERNAL
772                             || destination == DESTINATION_FILE_URI
773                             || destination == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
774                             && isFilenameValidInKnownPublicDir(filePath)) {
775                         idsToOrphan.add(downloadId);
776                     } else {
777                         idsToDelete.add(downloadId);
778                     }
779                 }
780             }
781         }
782 
783         if (idsToOrphan.size() > 0) {
784             Log.i(Constants.TAG, "Orphaning downloads with ids "
785                     + Arrays.toString(idsToOrphan.toArray()) + " as owner package is removed");
786             final ContentValues values = new ContentValues();
787             values.putNull(Constants.UID);
788             downloadProvider.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
789                     Helpers.buildQueryWithIds(idsToOrphan), null);
790         }
791         if (idsToDelete.size() > 0) {
792             Log.i(Constants.TAG, "Deleting downloads with ids "
793                     + Arrays.toString(idsToDelete.toArray()) + " as owner package is removed");
794             downloadProvider.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
795                     Helpers.buildQueryWithIds(idsToDelete), null);
796         }
797     }
798 
buildQueryWithIds(ArrayList<Long> downloadIds)799     public static String buildQueryWithIds(ArrayList<Long> downloadIds) {
800         final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in (");
801         final int size = downloadIds.size();
802         for (int i = 0; i < size; i++) {
803             queryBuilder.append(downloadIds.get(i));
804             queryBuilder.append((i == size - 1) ? ")" : ",");
805         }
806         return queryBuilder.toString();
807     }
808 
getPackageForUid(Context context, int uid)809     public static String getPackageForUid(Context context, int uid) {
810         String[] packages = context.getPackageManager().getPackagesForUid(uid);
811         if (packages == null || packages.length == 0) {
812             return null;
813         }
814         // For permission related purposes, any package belonging to the given uid should work.
815         return packages[0];
816     }
817 }
818