1 // Copyright 2016 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.download; 6 7 import android.app.Activity; 8 import android.app.DownloadManager; 9 import android.content.ActivityNotFoundException; 10 import android.content.Context; 11 import android.content.Intent; 12 import android.content.pm.PackageInfo; 13 import android.content.pm.PackageManager; 14 import android.net.Uri; 15 import android.os.Build; 16 import android.text.TextUtils; 17 18 import androidx.annotation.MainThread; 19 import androidx.annotation.Nullable; 20 21 import org.chromium.base.ApplicationStatus; 22 import org.chromium.base.ContentUriUtils; 23 import org.chromium.base.ContextUtils; 24 import org.chromium.base.FileUtils; 25 import org.chromium.base.IntentUtils; 26 import org.chromium.base.Log; 27 import org.chromium.base.annotations.CalledByNative; 28 import org.chromium.base.annotations.NativeMethods; 29 import org.chromium.base.metrics.RecordHistogram; 30 import org.chromium.base.metrics.RecordUserAction; 31 import org.chromium.chrome.R; 32 import org.chromium.chrome.browser.ChromeTabbedActivity; 33 import org.chromium.chrome.browser.IntentHandler; 34 import org.chromium.chrome.browser.document.ChromeIntentUtil; 35 import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory; 36 import org.chromium.chrome.browser.feature_engagement.TrackerFactory; 37 import org.chromium.chrome.browser.flags.ChromeFeatureList; 38 import org.chromium.chrome.browser.media.MediaViewerUtils; 39 import org.chromium.chrome.browser.offlinepages.DownloadUiActionFlags; 40 import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; 41 import org.chromium.chrome.browser.offlinepages.OfflinePageOrigin; 42 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; 43 import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge; 44 import org.chromium.chrome.browser.profiles.Profile; 45 import org.chromium.chrome.browser.tab.Tab; 46 import org.chromium.chrome.browser.tab.TabLaunchType; 47 import org.chromium.chrome.browser.tabmodel.document.TabDelegate; 48 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil; 49 import org.chromium.components.download.DownloadState; 50 import org.chromium.components.download.ResumeMode; 51 import org.chromium.components.embedder_support.util.UrlConstants; 52 import org.chromium.components.feature_engagement.EventConstants; 53 import org.chromium.components.feature_engagement.Tracker; 54 import org.chromium.components.offline_items_collection.ContentId; 55 import org.chromium.components.offline_items_collection.FailState; 56 import org.chromium.components.offline_items_collection.LaunchLocation; 57 import org.chromium.components.offline_items_collection.LegacyHelpers; 58 import org.chromium.components.offline_items_collection.OfflineItem; 59 import org.chromium.components.offline_items_collection.OpenParams; 60 import org.chromium.content_public.browser.BrowserStartupController; 61 import org.chromium.content_public.browser.LoadUrlParams; 62 import org.chromium.ui.base.DeviceFormFactor; 63 import org.chromium.ui.widget.Toast; 64 65 import java.io.File; 66 67 /** 68 * A class containing some utility static methods. 69 */ 70 public class DownloadUtils { 71 private static final String TAG = "download"; 72 73 private static final String EXTRA_IS_OFF_THE_RECORD = 74 "org.chromium.chrome.browser.download.IS_OFF_THE_RECORD"; 75 private static final String MIME_TYPE_ZIP = "application/zip"; 76 private static final String DOCUMENTS_UI_PACKAGE_NAME = "com.android.documentsui"; 77 public static final String EXTRA_SHOW_PREFETCHED_CONTENT = 78 "org.chromium.chrome.browser.download.SHOW_PREFETCHED_CONTENT"; 79 80 /** 81 * Displays the download manager UI. Note the UI is different on tablets and on phones. 82 * @param activity The current activity is available. 83 * @param tab The current tab if it exists. 84 * @param source The source where the user action is coming from. 85 * @return Whether the UI was shown. 86 */ showDownloadManager( @ullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source)87 public static boolean showDownloadManager( 88 @Nullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source) { 89 return showDownloadManager(activity, tab, source, false); 90 } 91 92 /** 93 * Displays the download manager UI. Note the UI is different on tablets and on phones. 94 * @param activity The current activity is available. 95 * @param tab The current tab if it exists. 96 * @param source The source where the user action is coming from. 97 * @param showPrefetchedContent Whether the manager should start with prefetched content section 98 * expanded. 99 * @return Whether the UI was shown. 100 */ 101 @CalledByNative showDownloadManager(@ullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source, boolean showPrefetchedContent)102 public static boolean showDownloadManager(@Nullable Activity activity, @Nullable Tab tab, 103 @DownloadOpenSource int source, boolean showPrefetchedContent) { 104 // Figure out what tab was last being viewed by the user. 105 if (activity == null) activity = ApplicationStatus.getLastTrackedFocusedActivity(); 106 Context appContext = ContextUtils.getApplicationContext(); 107 boolean isTablet; 108 109 if (tab == null && activity instanceof ChromeTabbedActivity) { 110 ChromeTabbedActivity chromeActivity = ((ChromeTabbedActivity) activity); 111 tab = chromeActivity.getActivityTab(); 112 isTablet = chromeActivity.isTablet(); 113 } else { 114 Context displayContext = activity != null ? activity : appContext; 115 isTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(displayContext); 116 } 117 118 if (isTablet) { 119 // Download Home shows up as a tab on tablets. 120 LoadUrlParams params = new LoadUrlParams(UrlConstants.DOWNLOADS_URL); 121 if (tab == null || !tab.isInitialized()) { 122 // Open a new tab, which pops Chrome into the foreground. 123 TabDelegate delegate = new TabDelegate(false); 124 delegate.createNewTab(params, TabLaunchType.FROM_CHROME_UI, null); 125 } else { 126 // Download Home shows up inside an existing tab, but only if the last Activity was 127 // the ChromeTabbedActivity. 128 tab.loadUrl(params); 129 130 // Bring Chrome to the foreground, if possible. 131 Intent intent = ChromeIntentUtil.createBringTabToFrontIntent(tab.getId()); 132 if (intent != null) { 133 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 134 IntentUtils.safeStartActivity(appContext, intent); 135 } 136 } 137 } else { 138 // Download Home shows up as a new Activity on phones. 139 Intent intent = new Intent(); 140 intent.setClass(appContext, DownloadActivity.class); 141 intent.putExtra(EXTRA_SHOW_PREFETCHED_CONTENT, showPrefetchedContent); 142 if (tab != null) intent.putExtra(EXTRA_IS_OFF_THE_RECORD, tab.isIncognito()); 143 if (activity == null) { 144 // Stands alone in its own task. 145 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 146 appContext.startActivity(intent); 147 } else { 148 // Sits on top of another Activity. 149 intent.addFlags( 150 Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); 151 intent.putExtra(IntentHandler.EXTRA_PARENT_COMPONENT, activity.getComponentName()); 152 activity.startActivity(intent); 153 } 154 } 155 156 if (BrowserStartupController.getInstance().isFullBrowserStarted()) { 157 // TODO (https://crbug.com/1048632): Use the current profile (i.e., regular profile or 158 // incognito profile) instead of always using regular profile. It works correctly now, 159 // but it is not safe. 160 Profile profile = (tab == null ? Profile.getLastUsedRegularProfile() 161 : Profile.fromWebContents(tab.getWebContents())); 162 Tracker tracker = TrackerFactory.getTrackerForProfile(profile); 163 tracker.notifyEvent(EventConstants.DOWNLOAD_HOME_OPENED); 164 } 165 DownloadMetrics.recordDownloadPageOpen(source); 166 return true; 167 } 168 169 /** 170 * @return Whether or not the Intent corresponds to a DownloadActivity that should show off the 171 * record downloads. 172 */ shouldShowOffTheRecordDownloads(Intent intent)173 public static boolean shouldShowOffTheRecordDownloads(Intent intent) { 174 return IntentUtils.safeGetBooleanExtra(intent, EXTRA_IS_OFF_THE_RECORD, false); 175 } 176 177 /** 178 * @return Whether or not the prefetched content section should be expanded on launch of the 179 * DownloadActivity. 180 */ shouldShowPrefetchContent(Intent intent)181 public static boolean shouldShowPrefetchContent(Intent intent) { 182 return IntentUtils.safeGetBooleanExtra(intent, EXTRA_SHOW_PREFETCHED_CONTENT, false); 183 } 184 185 /** 186 * @return Whether or not pagination headers should be shown on download home. 187 */ shouldShowPaginationHeaders()188 public static boolean shouldShowPaginationHeaders() { 189 return ChromeAccessibilityUtil.get().isAccessibilityEnabled() 190 || ChromeAccessibilityUtil.isHardwareKeyboardAttached( 191 ContextUtils.getApplicationContext().getResources().getConfiguration()); 192 } 193 194 /** 195 * Records metrics related to downloading a page. Should be called after a tap on the download 196 * page button. 197 * @param tab The Tab containing the page being downloaded. 198 */ recordDownloadPageMetrics(Tab tab)199 public static void recordDownloadPageMetrics(Tab tab) { 200 RecordHistogram.recordPercentageHistogram( 201 "OfflinePages.SavePage.PercentLoaded", Math.round(tab.getProgress() * 100)); 202 } 203 204 /** 205 * Shows a "Downloading..." toast. Should be called after a download has been started. 206 * @param context The {@link Context} used to make the toast. 207 */ showDownloadStartToast(Context context)208 public static void showDownloadStartToast(Context context) { 209 Toast.makeText(context, R.string.download_started, Toast.LENGTH_SHORT).show(); 210 } 211 212 /** 213 * Issues a request to the {@link DownloadManagerService} associated to check for externally 214 * removed downloads. 215 * See {@link DownloadManagerService#checkForExternallyRemovedDownloads}. 216 * @param isOffTheRecord Whether to check downloads for the off the record profile. 217 */ checkForExternallyRemovedDownloads(boolean isOffTheRecord)218 public static void checkForExternallyRemovedDownloads(boolean isOffTheRecord) { 219 if (ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_OFFLINE_CONTENT_PROVIDER)) { 220 return; 221 } 222 223 if (isOffTheRecord) { 224 DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( 225 true); 226 } 227 DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads( 228 false); 229 RecordUserAction.record( 230 "Android.DownloadManager.CheckForExternallyRemovedItems"); 231 } 232 233 /** 234 * Trigger the download of an Offline Page. 235 * @param context Context to pull resources from. 236 */ downloadOfflinePage(Context context, Tab tab)237 public static void downloadOfflinePage(Context context, Tab tab) { 238 OfflinePageOrigin origin = new OfflinePageOrigin(context, tab); 239 240 if (tab.isShowingErrorPage()) { 241 // The download needs to be scheduled to happen at later time due to current network 242 // error. 243 final OfflinePageBridge bridge = 244 OfflinePageBridge.getForProfile(Profile.fromWebContents(tab.getWebContents())); 245 bridge.scheduleDownload(tab.getWebContents(), OfflinePageBridge.ASYNC_NAMESPACE, 246 tab.getUrlString(), DownloadUiActionFlags.PROMPT_DUPLICATE, origin); 247 } else { 248 // Otherwise, the download can be started immediately. 249 OfflinePageDownloadBridge.startDownload(tab, origin); 250 DownloadUtils.recordDownloadPageMetrics(tab); 251 } 252 253 Tracker tracker = 254 TrackerFactory.getTrackerForProfile(Profile.fromWebContents(tab.getWebContents())); 255 tracker.notifyEvent(EventConstants.DOWNLOAD_PAGE_STARTED); 256 } 257 258 /** 259 * Whether the user should be allowed to download the current page. 260 * @param tab Tab displaying the page that will be downloaded. 261 * @return Whether the "Download Page" button should be enabled. 262 */ isAllowedToDownloadPage(Tab tab)263 public static boolean isAllowedToDownloadPage(Tab tab) { 264 if (tab == null) return false; 265 266 // Offline pages isn't supported in Incognito. This should be checked before calling 267 // OfflinePageBridge.getForProfile because OfflinePageBridge instance will not be found 268 // for incognito profile. 269 if (tab.isIncognito()) return false; 270 271 // Check if the page url is supported for saving. Only HTTP and HTTPS pages are allowed. 272 if (!OfflinePageBridge.canSavePage(tab.getUrlString())) return false; 273 274 // Download will only be allowed for the error page if download button is shown in the page. 275 if (tab.isShowingErrorPage()) { 276 final OfflinePageBridge bridge = 277 OfflinePageBridge.getForProfile(Profile.fromWebContents(tab.getWebContents())); 278 return bridge.isShowingDownloadButtonInErrorPage(tab.getWebContents()); 279 } 280 281 // Don't allow re-downloading the currently displayed offline page. 282 if (OfflinePageUtils.isOfflinePage(tab)) return false; 283 284 return true; 285 } 286 287 /** 288 * Returns a URI that points at the file. 289 * @param filePath File path to get a URI for. 290 * @return URI that points at that file, either as a content:// URI or a file:// URI. 291 */ 292 @MainThread getUriForItem(String filePath)293 public static Uri getUriForItem(String filePath) { 294 if (ContentUriUtils.isContentUri(filePath)) return Uri.parse(filePath); 295 296 // It's ok to use blocking calls on main thread here, since the user is waiting to open or 297 // share the file to other apps. 298 boolean isOnSDCard = DownloadDirectoryProvider.isDownloadOnSDCard(filePath); 299 if (ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_FILE_PROVIDER) && isOnSDCard) { 300 // Use custom file provider to generate content URI for download on SD card. 301 return DownloadFileProvider.createContentUri(filePath); 302 } 303 // Use FileProvider to generate content URI or file URI. 304 return FileUtils.getUriForFile(new File(filePath)); 305 } 306 307 /** 308 * Get the URI when shared or opened by other apps. 309 * 310 * @param filePath Downloaded file path. 311 * @return URI for other apps to use the file via {@link android.content.ContentResolver}. 312 */ getUriForOtherApps(String filePath)313 public static Uri getUriForOtherApps(String filePath) { 314 // Some old Samsung devices with Android M- must use file URI. See https://crbug.com/705748. 315 return Build.VERSION.SDK_INT > Build.VERSION_CODES.M ? getUriForItem(filePath) 316 : Uri.fromFile(new File(filePath)); 317 } 318 319 @CalledByNative getUriStringForPath(String filePath)320 private static String getUriStringForPath(String filePath) { 321 if (ContentUriUtils.isContentUri(filePath)) return filePath; 322 Uri uri = getUriForItem(filePath); 323 return uri != null ? uri.toString() : new String(); 324 } 325 326 /** 327 * Utility method to open an {@link OfflineItem}, which can be a chrome download, offline page. 328 * Falls back to open download home. 329 * @param contentId The {@link ContentId} of the associated offline item. 330 * @param isOffTheRecord Whether the download should be opened in incognito mode. 331 * @param source The location from which the download was opened. 332 */ openItem( ContentId contentId, boolean isOffTheRecord, @DownloadOpenSource int source)333 public static void openItem( 334 ContentId contentId, boolean isOffTheRecord, @DownloadOpenSource int source) { 335 if (LegacyHelpers.isLegacyAndroidDownload(contentId)) { 336 ContextUtils.getApplicationContext().startActivity( 337 new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) 338 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 339 } else if (LegacyHelpers.isLegacyOfflinePage(contentId)) { 340 OpenParams openParams = new OpenParams(LaunchLocation.PROGRESS_BAR); 341 openParams.openInIncognito = isOffTheRecord; 342 OfflineContentAggregatorFactory.get().openItem(openParams, contentId); 343 } else { 344 DownloadManagerService.getDownloadManagerService().openDownload( 345 contentId, isOffTheRecord, source); 346 } 347 } 348 349 /** 350 * Opens a file in Chrome or in another app if appropriate. 351 * @param filePath Path to the file to open, can be a content Uri. 352 * @param mimeType mime type of the file. 353 * @param downloadGuid The associated download GUID. 354 * @param isOffTheRecord whether we are in an off the record context. 355 * @param originalUrl The original url of the downloaded file. 356 * @param referrer Referrer of the downloaded file. 357 * @param source The source that tries to open the download file. 358 * @return whether the file could successfully be opened. 359 */ openFile(String filePath, String mimeType, String downloadGuid, boolean isOffTheRecord, String originalUrl, String referrer, @DownloadOpenSource int source)360 public static boolean openFile(String filePath, String mimeType, String downloadGuid, 361 boolean isOffTheRecord, String originalUrl, String referrer, 362 @DownloadOpenSource int source) { 363 DownloadMetrics.recordDownloadOpen(source, mimeType); 364 Context context = ContextUtils.getApplicationContext(); 365 DownloadManagerService service = DownloadManagerService.getDownloadManagerService(); 366 367 // Check if Chrome should open the file itself. 368 if (service.isDownloadOpenableInBrowser(isOffTheRecord, mimeType)) { 369 // Share URIs use the content:// scheme when able, which looks bad when displayed 370 // in the URL bar. 371 Uri contentUri = getUriForItem(filePath); 372 Uri fileUri = contentUri; 373 if (!ContentUriUtils.isContentUri(filePath)) { 374 File file = new File(filePath); 375 fileUri = Uri.fromFile(file); 376 } 377 String normalizedMimeType = Intent.normalizeMimeType(mimeType); 378 379 Intent intent = MediaViewerUtils.getMediaViewerIntent(fileUri /*displayUri*/, 380 contentUri /*contentUri*/, normalizedMimeType, 381 true /* allowExternalAppHandlers */); 382 IntentHandler.startActivityForTrustedIntent(intent); 383 service.updateLastAccessTime(downloadGuid, isOffTheRecord); 384 return true; 385 } 386 387 // Check if any apps can open the file. 388 try { 389 // TODO(qinmin): Move this to an AsyncTask so we don't need to temper with strict mode. 390 Uri uri = ContentUriUtils.isContentUri(filePath) ? Uri.parse(filePath) 391 : getUriForOtherApps(filePath); 392 Intent viewIntent = 393 MediaViewerUtils.createViewIntentForUri(uri, mimeType, originalUrl, referrer); 394 context.startActivity(viewIntent); 395 service.updateLastAccessTime(downloadGuid, isOffTheRecord); 396 return true; 397 } catch (Exception e) { 398 Log.e(TAG, "Cannot start activity to open file", e); 399 } 400 401 // If this is a zip file, check if Android Files app exists. 402 if (MIME_TYPE_ZIP.equals(mimeType)) { 403 try { 404 PackageInfo packageInfo = context.getPackageManager().getPackageInfo( 405 DOCUMENTS_UI_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); 406 if (packageInfo != null) { 407 Intent viewDownloadsIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS); 408 viewDownloadsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 409 viewDownloadsIntent.setPackage(DOCUMENTS_UI_PACKAGE_NAME); 410 context.startActivity(viewDownloadsIntent); 411 return true; 412 } 413 } catch (Exception e) { 414 Log.e(TAG, "Cannot find files app for openning zip files", e); 415 } 416 } 417 // Can't launch the Intent. 418 if (source != DownloadOpenSource.DOWNLOAD_PROGRESS_INFO_BAR) { 419 Toast.makeText(context, context.getString(R.string.download_cant_open_file), 420 Toast.LENGTH_SHORT) 421 .show(); 422 } 423 return false; 424 } 425 426 @CalledByNative openDownload(String filePath, String mimeType, String downloadGuid, boolean isOffTheRecord, String originalUrl, String referer, @DownloadOpenSource int source)427 private static void openDownload(String filePath, String mimeType, String downloadGuid, 428 boolean isOffTheRecord, String originalUrl, String referer, 429 @DownloadOpenSource int source) { 430 boolean canOpen = DownloadUtils.openFile( 431 filePath, mimeType, downloadGuid, isOffTheRecord, originalUrl, referer, source); 432 if (!canOpen) { 433 DownloadUtils.showDownloadManager(null, null, source); 434 } 435 } 436 437 /** 438 * Fires an Intent to open a downloaded item. 439 * @param context Context to use. 440 * @param intent Intent that can be fired. 441 * @return Whether an Activity was successfully started for the Intent. 442 */ fireOpenIntentForDownload(Context context, Intent intent)443 static boolean fireOpenIntentForDownload(Context context, Intent intent) { 444 try { 445 if (TextUtils.equals(intent.getPackage(), context.getPackageName())) { 446 IntentHandler.startActivityForTrustedIntent(intent); 447 } else { 448 context.startActivity(intent); 449 } 450 return true; 451 } catch (ActivityNotFoundException ex) { 452 Log.d(TAG, "Activity not found for " + intent.getType() + " over " 453 + intent.getData().getScheme(), ex); 454 } catch (SecurityException ex) { 455 Log.d(TAG, "cannot open intent: " + intent, ex); 456 } catch (Exception ex) { 457 Log.d(TAG, "cannot open intent: " + intent, ex); 458 } 459 460 return false; 461 } 462 463 /** 464 * Get the resume mode based on the current fail state, to distinguish the case where download 465 * cannot be resumed at all or can be resumed in the middle, or should be restarted from the 466 * beginning. 467 * @param url URL of the download. 468 * @param failState Why the download failed. 469 * @return The resume mode for the current fail state. 470 */ getResumeMode(String url, @FailState int failState)471 public static @ResumeMode int getResumeMode(String url, @FailState int failState) { 472 return DownloadUtilsJni.get().getResumeMode(url, failState); 473 } 474 475 /** 476 * Query the Download backends about whether a download is paused. 477 * 478 * The Java-side contains more information about the status of a download than is persisted 479 * by the native backend, so it is queried first. 480 * 481 * @param item Download to check the status of. 482 * @return Whether the download is paused or not. 483 */ isDownloadPaused(DownloadItem item)484 public static boolean isDownloadPaused(DownloadItem item) { 485 DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance(); 486 DownloadSharedPreferenceEntry entry = 487 helper.getDownloadSharedPreferenceEntry(item.getContentId()); 488 489 if (entry != null) { 490 // The Java downloads backend knows more about the download than the native backend. 491 return !entry.isAutoResumable; 492 } else { 493 // Only the native downloads backend knows about the download. 494 if (item.getDownloadInfo().state() == DownloadState.IN_PROGRESS) { 495 return item.getDownloadInfo().isPaused(); 496 } else { 497 return item.getDownloadInfo().state() == DownloadState.INTERRUPTED; 498 } 499 } 500 } 501 502 /** 503 * Return whether a download is pending. 504 * @param item Download to check the status of. 505 * @return Whether the download is pending or not. 506 */ isDownloadPending(DownloadItem item)507 public static boolean isDownloadPending(DownloadItem item) { 508 DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance(); 509 DownloadSharedPreferenceEntry entry = 510 helper.getDownloadSharedPreferenceEntry(item.getContentId()); 511 return entry != null && item.getDownloadInfo().state() == DownloadState.INTERRUPTED 512 && entry.isAutoResumable; 513 } 514 515 @NativeMethods 516 interface Natives { getResumeMode(String url, @FailState int failState)517 int getResumeMode(String url, @FailState int failState); 518 } 519 } 520