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