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