| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/download/download_manager_coordinator.h" |
| |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <StoreKit/StoreKit.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/check_op.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ios/chrome/browser/download/confirm_download_closing_overlay.h" |
| #include "ios/chrome/browser/download/confirm_download_replacing_overlay.h" |
| #include "ios/chrome/browser/download/download_directory_util.h" |
| #include "ios/chrome/browser/download/download_manager_metric_names.h" |
| #import "ios/chrome/browser/download/download_manager_tab_helper.h" |
| #import "ios/chrome/browser/download/external_app_util.h" |
| #import "ios/chrome/browser/installation_notifier.h" |
| #import "ios/chrome/browser/main/browser.h" |
| #import "ios/chrome/browser/overlays/public/common/confirmation/confirmation_overlay_response.h" |
| #include "ios/chrome/browser/overlays/public/overlay_callback_manager.h" |
| #import "ios/chrome/browser/overlays/public/overlay_request_queue.h" |
| #import "ios/chrome/browser/store_kit/store_kit_coordinator.h" |
| #import "ios/chrome/browser/ui/commands/browser_coordinator_commands.h" |
| #import "ios/chrome/browser/ui/commands/command_dispatcher.h" |
| #import "ios/chrome/browser/ui/download/activities/open_downloads_folder_activity.h" |
| #import "ios/chrome/browser/ui/download/download_manager_mediator.h" |
| #import "ios/chrome/browser/ui/download/download_manager_view_controller.h" |
| #import "ios/chrome/browser/ui/presenters/contained_presenter.h" |
| #import "ios/chrome/browser/ui/presenters/contained_presenter_delegate.h" |
| #include "ios/chrome/browser/ui/util/ui_util.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list_observer.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/web/public/download/download_task.h" |
| #include "net/base/net_errors.h" |
| #include "net/url_request/url_fetcher_response_writer.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| // Tracks download tasks which were not opened by the user yet. Reports various |
| // metrics in DownloadTaskObserver callbacks. |
| class UnopenedDownloadsTracker : public web::DownloadTaskObserver, |
| public WebStateListObserver { |
| public: |
| // Starts tracking this download task. |
| void Add(web::DownloadTask* task) { |
| task->AddObserver(this); |
| observed_tasks_.insert(task); |
| } |
| // Stops tracking this download task. |
| void Remove(web::DownloadTask* task) { |
| task->RemoveObserver(this); |
| observed_tasks_.erase(task); |
| } |
| // DownloadTaskObserver overrides: |
| void OnDownloadUpdated(web::DownloadTask* task) override { |
| if (task->IsDone()) { |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileResult", |
| task->GetErrorCode() |
| ? DownloadFileResult::Failure |
| : DownloadFileResult::Completed, |
| DownloadFileResult::Count); |
| if (task->GetErrorCode()) { |
| base::UmaHistogramSparse("Download.IOSDownloadedFileNetError", |
| -task->GetErrorCode()); |
| } else { |
| bool GoogleDriveIsInstalled = IsGoogleDriveAppInstalled(); |
| if (GoogleDriveIsInstalled) |
| base::UmaHistogramEnumeration( |
| "Download.IOSDownloadFileUIGoogleDrive", |
| DownloadFileUIGoogleDrive::GoogleDriveAlreadyInstalled, |
| DownloadFileUIGoogleDrive::Count); |
| else |
| base::UmaHistogramEnumeration( |
| "Download.IOSDownloadFileUIGoogleDrive", |
| DownloadFileUIGoogleDrive::GoogleDriveNotInstalled, |
| DownloadFileUIGoogleDrive::Count); |
| } |
| |
| bool backgrounded = task->HasPerformedBackgroundDownload(); |
| DownloadFileInBackground histogram_value = |
| task->GetErrorCode() |
| ? (backgrounded |
| ? DownloadFileInBackground::FailedWithBackgrounding |
| : DownloadFileInBackground::FailedWithoutBackgrounding) |
| : (backgrounded |
| ? DownloadFileInBackground::SucceededWithBackgrounding |
| : DownloadFileInBackground::SucceededWithoutBackgrounding); |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileInBackground", |
| histogram_value, |
| DownloadFileInBackground::Count); |
| } |
| } |
| void OnDownloadDestroyed(web::DownloadTask* task) override { |
| // This download task was never open by the user. |
| task->RemoveObserver(this); |
| observed_tasks_.erase(task); |
| |
| DownloadAborted(task); |
| } |
| |
| // Logs histograms. Called when DownloadTask or this object was destroyed. |
| void DownloadAborted(web::DownloadTask* task) { |
| if (task->GetState() == web::DownloadTask::State::kInProgress) { |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileResult", |
| DownloadFileResult::Other, |
| DownloadFileResult::Count); |
| |
| if (did_close_web_state_without_user_action) { |
| // web state can be closed without user action only during the app |
| // shutdown. |
| base::UmaHistogramEnumeration( |
| "Download.IOSDownloadFileInBackground", |
| DownloadFileInBackground::CanceledAfterAppQuit, |
| DownloadFileInBackground::Count); |
| } |
| } |
| |
| if (task->IsDone() && task->GetErrorCode() == net::OK) { |
| base::UmaHistogramEnumeration( |
| "Download.IOSDownloadedFileAction", |
| DownloadedFileAction::NoActionOrOpenedViaExtension, |
| DownloadedFileAction::Count); |
| } |
| } |
| // WebStateListObserver overrides: |
| void WillCloseWebStateAt(WebStateList* web_state_list, |
| web::WebState* web_state, |
| int index, |
| bool user_action) override { |
| if (!user_action) { |
| did_close_web_state_without_user_action = true; |
| } |
| } |
| |
| ~UnopenedDownloadsTracker() override { |
| for (web::DownloadTask* task : observed_tasks_) { |
| task->RemoveObserver(this); |
| DownloadAborted(task); |
| } |
| } |
| |
| private: |
| // True if a web state was closed without user action. |
| bool did_close_web_state_without_user_action = false; |
| // Keeps track of observed tasks to remove observer when |
| // UnopenedDownloadsTracker is destructed. |
| std::set<web::DownloadTask*> observed_tasks_; |
| }; |
| } // namespace |
| |
| @interface DownloadManagerCoordinator () < |
| ContainedPresenterDelegate, |
| DownloadManagerViewControllerDelegate> { |
| // View controller for presenting Download Manager UI. |
| DownloadManagerViewController* _viewController; |
| // View controller for presenting "Open In.." dialog. |
| UIActivityViewController* _openInController; |
| DownloadManagerMediator _mediator; |
| StoreKitCoordinator* _storeKitCoordinator; |
| UnopenedDownloadsTracker _unopenedDownloads; |
| } |
| @end |
| |
| @implementation DownloadManagerCoordinator |
| |
| @synthesize presenter = _presenter; |
| @synthesize animatesPresentation = _animatesPresentation; |
| @synthesize downloadTask = _downloadTask; |
| @synthesize bottomMarginHeightAnchor = _bottomMarginHeightAnchor; |
| |
| - (void)dealloc { |
| [self stop]; |
| [[InstallationNotifier sharedInstance] unregisterForNotifications:self]; |
| } |
| |
| - (void)start { |
| DCHECK(self.presenter); |
| DCHECK(self.browser); |
| |
| _viewController = [[DownloadManagerViewController alloc] init]; |
| _viewController.delegate = self; |
| _viewController.bottomMarginHeightAnchor = self.bottomMarginHeightAnchor; |
| _mediator.SetDownloadTask(_downloadTask); |
| _mediator.SetConsumer(_viewController); |
| |
| self.presenter.baseViewController = self.baseViewController; |
| self.presenter.presentedViewController = _viewController; |
| self.presenter.delegate = self; |
| |
| self.browser->GetWebStateList()->AddObserver(&_unopenedDownloads); |
| |
| [self.presenter prepareForPresentation]; |
| |
| [self.presenter presentAnimated:self.animatesPresentation]; |
| } |
| |
| - (void)stop { |
| if (_viewController) { |
| [self.presenter dismissAnimated:self.animatesPresentation]; |
| // Prevent delegate callbacks for stopped coordinator. |
| _viewController.delegate = nil; |
| _viewController = nil; |
| } |
| |
| _downloadTask = nullptr; |
| |
| if (self.browser) |
| (self.browser->GetWebStateList())->RemoveObserver(&_unopenedDownloads); |
| |
| [_storeKitCoordinator stop]; |
| _storeKitCoordinator = nil; |
| } |
| |
| - (UIViewController*)viewController { |
| return _viewController; |
| } |
| |
| #pragma mark - DownloadManagerTabHelperDelegate |
| |
| - (void)downloadManagerTabHelper:(nonnull DownloadManagerTabHelper*)tabHelper |
| didCreateDownload:(nonnull web::DownloadTask*)download |
| webStateIsVisible:(BOOL)webStateIsVisible { |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileUI", |
| DownloadFileUI::DownloadFileStarted, |
| DownloadFileUI::Count); |
| |
| if (!webStateIsVisible) { |
| // Do nothing if a background Tab requested download UI presentation. |
| return; |
| } |
| |
| BOOL replacingExistingDownload = _downloadTask ? YES : NO; |
| _downloadTask = download; |
| |
| if (replacingExistingDownload) { |
| _mediator.SetDownloadTask(_downloadTask); |
| } else { |
| self.animatesPresentation = YES; |
| [self start]; |
| } |
| } |
| |
| - (void)downloadManagerTabHelper:(nonnull DownloadManagerTabHelper*)tabHelper |
| decidePolicyForDownload:(nonnull web::DownloadTask*)download |
| completionHandler:(nonnull void (^)(NewDownloadPolicy))handler { |
| std::unique_ptr<OverlayRequest> request = |
| OverlayRequest::CreateWithConfig<ConfirmDownloadReplacingRequest>(); |
| |
| request->GetCallbackManager()->AddCompletionCallback( |
| base::BindOnce(^(OverlayResponse* response) { |
| // |response| is null if WebState was destroyed. Don't call completion |
| // handler if no buttons were tapped. |
| if (response) { |
| bool confirmed = |
| response->GetInfo<ConfirmationOverlayResponse>()->confirmed(); |
| base::UmaHistogramBoolean("Download.IOSDownloadReplaced", confirmed); |
| handler(confirmed ? kNewDownloadPolicyReplace |
| : kNewDownloadPolicyDiscard); |
| } |
| })); |
| |
| web::WebState* webState = download->GetWebState(); |
| OverlayRequestQueue::FromWebState(webState, OverlayModality::kWebContentArea) |
| ->AddRequest(std::move(request)); |
| } |
| |
| - (void)downloadManagerTabHelper:(nonnull DownloadManagerTabHelper*)tabHelper |
| didHideDownload:(nonnull web::DownloadTask*)download { |
| DCHECK_EQ(_downloadTask, download); |
| self.animatesPresentation = NO; |
| [self stop]; |
| self.animatesPresentation = YES; |
| } |
| |
| - (void)downloadManagerTabHelper:(nonnull DownloadManagerTabHelper*)tabHelper |
| didShowDownload:(nonnull web::DownloadTask*)download { |
| DCHECK_NE(_downloadTask, download); |
| _downloadTask = download; |
| self.animatesPresentation = NO; |
| [self start]; |
| self.animatesPresentation = YES; |
| } |
| |
| #pragma mark - ContainedPresenterDelegate |
| |
| - (void)containedPresenterDidPresent:(id<ContainedPresenter>)presenter { |
| DCHECK(presenter == self.presenter); |
| } |
| |
| - (void)containedPresenterDidDismiss:(id<ContainedPresenter>)presenter { |
| DCHECK(presenter == self.presenter); |
| } |
| |
| #pragma mark - DownloadManagerViewControllerDelegate |
| |
| - (void)downloadManagerViewControllerDidClose: |
| (DownloadManagerViewController*)controller { |
| if (_downloadTask->GetState() != web::DownloadTask::State::kInProgress) { |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileResult", |
| DownloadFileResult::NotStarted, |
| DownloadFileResult::Count); |
| base::RecordAction(base::UserMetricsAction("IOSDownloadClose")); |
| [self cancelDownload]; |
| return; |
| } |
| base::RecordAction( |
| base::UserMetricsAction("IOSDownloadTryCloseWhenInProgress")); |
| |
| std::unique_ptr<OverlayRequest> request = |
| OverlayRequest::CreateWithConfig<ConfirmDownloadClosingRequest>(); |
| |
| __weak DownloadManagerCoordinator* weakSelf = self; |
| request->GetCallbackManager()->AddCompletionCallback( |
| base::BindOnce(^(OverlayResponse* response) { |
| if (response && |
| response->GetInfo<ConfirmationOverlayResponse>()->confirmed()) { |
| base::UmaHistogramEnumeration("Download.IOSDownloadFileResult", |
| DownloadFileResult::Cancelled, |
| DownloadFileResult::Count); |
| [weakSelf cancelDownload]; |
| } |
| })); |
| |
| web::WebState* webState = self.downloadTask->GetWebState(); |
| OverlayRequestQueue::FromWebState(webState, OverlayModality::kWebContentArea) |
| ->AddRequest(std::move(request)); |
| } |
| |
| - (void)installDriveForDownloadManagerViewController: |
| (DownloadManagerViewController*)controller { |
| base::RecordAction(base::UserMetricsAction("IOSDownloadInstallGoogleDrive")); |
| [self presentStoreKitForGoogleDriveApp]; |
| } |
| |
| - (void)downloadManagerViewControllerDidStartDownload: |
| (DownloadManagerViewController*)controller { |
| if (_downloadTask->GetErrorCode() != net::OK) { |
| base::RecordAction(base::UserMetricsAction("MobileDownloadRetryDownload")); |
| } else { |
| base::RecordAction(base::UserMetricsAction("IOSDownloadStartDownload")); |
| _unopenedDownloads.Add(_downloadTask); |
| } |
| _mediator.StartDowloading(); |
| } |
| |
| - (void)presentOpenInForDownloadManagerViewController: |
| (DownloadManagerViewController*)controller { |
| base::RecordAction(base::UserMetricsAction("IOSDownloadOpenIn")); |
| base::FilePath path = _mediator.GetDownloadPath(); |
| NSURL* URL = [NSURL fileURLWithPath:base::SysUTF8ToNSString(path.value())]; |
| |
| NSArray* customActions = @[ URL ]; |
| NSArray* activities = nil; |
| |
| OpenDownloadsFolderActivity* customActivity = |
| [[OpenDownloadsFolderActivity alloc] init]; |
| customActivity.browserHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), BrowserCoordinatorCommands); |
| activities = @[ customActivity ]; |
| |
| _openInController = |
| [[UIActivityViewController alloc] initWithActivityItems:customActions |
| applicationActivities:activities]; |
| |
| _openInController.excludedActivityTypes = |
| @[ UIActivityTypeCopyToPasteboard, UIActivityTypeSaveToCameraRoll ]; |
| |
| // UIActivityViewController is presented in a popover on iPad. |
| _openInController.popoverPresentationController.sourceView = |
| _viewController.actionButton; |
| _openInController.popoverPresentationController.sourceRect = |
| _viewController.actionButton.bounds; |
| [_viewController presentViewController:_openInController |
| animated:YES |
| completion:nil]; |
| } |
| |
| #pragma mark - Private |
| |
| // Cancels the download task and stops the coordinator. |
| - (void)cancelDownload { |
| // |stop| nulls-our _downloadTask and |Cancel| destroys the task. Call |stop| |
| // first to perform all coordinator cleanups, but retain |_downloadTask| |
| // pointer to destroy the task. |
| web::DownloadTask* downloadTask = _downloadTask; |
| [self stop]; |
| downloadTask->Cancel(); |
| } |
| |
| // Called when Google Drive app is installed after starting StoreKitCoordinator. |
| - (void)didInstallGoogleDriveApp { |
| base::UmaHistogramEnumeration( |
| "Download.IOSDownloadFileUIGoogleDrive", |
| DownloadFileUIGoogleDrive::GoogleDriveInstalledAfterDisplay, |
| DownloadFileUIGoogleDrive::Count); |
| } |
| |
| // Presents StoreKit dialog for Google Drive application. |
| - (void)presentStoreKitForGoogleDriveApp { |
| if (!_storeKitCoordinator) { |
| _storeKitCoordinator = [[StoreKitCoordinator alloc] |
| initWithBaseViewController:self.baseViewController |
| browser:self.browser]; |
| _storeKitCoordinator.iTunesProductParameters = @{ |
| SKStoreProductParameterITunesItemIdentifier : |
| kGoogleDriveITunesItemIdentifier |
| }; |
| } |
| [_storeKitCoordinator start]; |
| [_viewController setInstallDriveButtonVisible:NO animated:YES]; |
| |
| [[InstallationNotifier sharedInstance] |
| registerForInstallationNotifications:self |
| withSelector:@selector(didInstallGoogleDriveApp) |
| forScheme:kGoogleDriveAppURLScheme]; |
| } |
| |
| @end |