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