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