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