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