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