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