blob: 2d23d1b3a73650a16284ad07c34afa2ee1882f4c [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/download/download_ui_controller.h"
#include <memory>
#include <utility>
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "build/build_config.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/download/bubble/download_bubble_utils.h"
#include "chrome/browser/download/download_crx_util.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_shelf.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/common/pref_names.h"
#include "components/download/public/common/download_item.h"
#include "components/security_state/content/security_state_tab_helper.h"
#include "components/security_state/core/security_state.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/strings/string_util.h"
#include "chrome/browser/download/android/download_controller.h"
#include "chrome/browser/download/android/download_controller_base.h"
#include "components/pdf/common/constants.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/common/content_features.h"
#else
#include "chrome/browser/download/bubble/download_bubble_prefs.h"
#include "chrome/browser/download/bubble/download_bubble_ui_controller.h"
#include "chrome/browser/download/bubble/download_bubble_update_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#endif
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/download/notification/download_notification_manager.h"
#endif // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
#include "components/download/public/common/desktop/desktop_auto_resumption_handler.h"
#include "components/download/public/common/download_features.h"
#endif
namespace {
#if BUILDFLAG(IS_ANDROID)
class AndroidUIControllerDelegate : public DownloadUIController::Delegate {
public:
AndroidUIControllerDelegate() = default;
~AndroidUIControllerDelegate() override = default;
private:
// DownloadUIController::Delegate
void OnNewDownloadReady(download::DownloadItem* item) override;
};
void AndroidUIControllerDelegate::OnNewDownloadReady(
download::DownloadItem* item) {
DownloadControllerBase::Get()->OnDownloadStarted(item);
}
#else // BUILDFLAG(IS_ANDROID)
void InitializeDownloadBubbleUpdateService(Profile* profile,
content::DownloadManager* manager) {
DownloadBubbleUpdateService* download_bubble_update_service =
DownloadBubbleUpdateServiceFactory::GetForProfile(profile);
if (!download_bubble_update_service) {
return;
}
download_bubble_update_service->Initialize(manager);
}
class DownloadBubbleUIControllerDelegate
: public DownloadUIController::Delegate {
public:
// |profile| is required to outlive DownloadBubbleUIControllerDelegate.
explicit DownloadBubbleUIControllerDelegate(Profile* profile)
: profile_(profile) {
if (profile_->IsOffTheRecord()) {
profile_->GetPrefs()->SetBoolean(prefs::kPromptForDownload, true);
}
}
~DownloadBubbleUIControllerDelegate() override = default;
private:
// DownloadUIController::Delegate
void OnNewDownloadReady(download::DownloadItem* item) override;
void OnButtonClicked() override;
raw_ptr<Profile> profile_;
};
void DownloadBubbleUIControllerDelegate::OnNewDownloadReady(
download::DownloadItem* item) {
// Here the item will be surfaced to the bubble UI and should
// subject to the auto resumption logic.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
if (download::features::IsBackoffInDownloadingEnabled()) {
auto* handler = download::DesktopAutoResumptionHandler::Get();
item->RemoveObserver(handler);
item->AddObserver(handler);
}
#endif
if (!DownloadItemModel(item).ShouldShowInBubble())
return;
// crx downloads are handled by the DownloadBubbleUpdateService.
// TODO(chlily): Consolidate these code paths.
if (download_crx_util::IsExtensionDownload(*item)) {
return;
}
DownloadBubbleUpdateService* download_bubble_update_service =
DownloadBubbleUpdateServiceFactory::GetForProfile(profile_);
if (!download_bubble_update_service) {
return;
}
download_bubble_update_service->NotifyWindowsOfDownloadItemAdded(item);
}
void DownloadBubbleUIControllerDelegate::OnButtonClicked() {
BrowserList* browser_list = BrowserList::GetInstance();
if (!browser_list)
return;
for (Browser* browser : *browser_list) {
if (browser && browser->window() &&
browser->window()->GetDownloadBubbleUIController()) {
browser->window()->GetDownloadBubbleUIController()->HandleButtonPressed();
}
}
}
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_CHROMEOS)
// A composite `DownloadUIController::Delegate` for use exclusively on ChromeOS.
// TODO(http://b/279791981): Remove after enabling the new downloads integration
// with System UI surfaces and deprecating `DownloadNotificationManager`.
class CrOSUIControllerDelegate : public DownloadUIController::Delegate {
public:
explicit CrOSUIControllerDelegate(content::DownloadManager* manager) {
// Conditionally add the `DownloadBubbleUIControllerDelegate`.
auto* profile = Profile::FromBrowserContext(manager->GetBrowserContext());
if (download::IsDownloadBubbleEnabled()) {
delegates_.emplace_back(
std::make_unique<DownloadBubbleUIControllerDelegate>(profile));
InitializeDownloadBubbleUpdateService(profile, manager);
}
// The `DownloadNotificationManager` should always be added as it provides
// System UI notifications on ChromeOS.
delegates_.emplace_back(
std::make_unique<DownloadNotificationManager>(profile));
}
CrOSUIControllerDelegate(const CrOSUIControllerDelegate&) = delete;
CrOSUIControllerDelegate& operator=(const CrOSUIControllerDelegate&) = delete;
~CrOSUIControllerDelegate() override = default;
private:
// DownloadUIController::Delegate:
void OnNewDownloadReady(download::DownloadItem* item) override {
for (auto& delegate : delegates_) {
delegate->OnNewDownloadReady(item);
}
}
void OnButtonClicked() override {
for (auto& delegate : delegates_) {
delegate->OnButtonClicked();
}
}
// The collection of delegates contained by this composite.
std::vector<std::unique_ptr<DownloadUIController::Delegate>> delegates_;
};
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace
DownloadUIController::Delegate::~Delegate() = default;
void DownloadUIController::Delegate::OnButtonClicked() {}
DownloadUIController::DownloadUIController(content::DownloadManager* manager,
std::unique_ptr<Delegate> delegate)
: download_notifier_(manager, this), delegate_(std::move(delegate)) {
#if BUILDFLAG(IS_ANDROID)
if (!delegate_)
delegate_ = std::make_unique<AndroidUIControllerDelegate>();
#elif BUILDFLAG(IS_CHROMEOS)
if (!delegate_) {
delegate_ = std::make_unique<CrOSUIControllerDelegate>(manager);
}
#else // BUILDFLAG(IS_CHROMEOS)
if (!delegate_) {
Profile* profile =
Profile::FromBrowserContext(manager->GetBrowserContext());
CHECK(download::IsDownloadBubbleEnabled());
delegate_ = std::make_unique<DownloadBubbleUIControllerDelegate>(profile);
InitializeDownloadBubbleUpdateService(profile, manager);
}
#endif // BUILDFLAG(IS_ANDROID)
}
DownloadUIController::~DownloadUIController() = default;
void DownloadUIController::OnButtonClicked() {
delegate_->OnButtonClicked();
}
void DownloadUIController::OnDownloadCreated(content::DownloadManager* manager,
download::DownloadItem* item) {
// Record the security level of the page triggering the download. Only record
// when the download occurs in the WebContents that initiated the download
// (e.g., not downloads in new tabs or windows, which have a different
// WebContents).
content::WebContents* web_contents =
content::DownloadItemUtils::GetWebContents(item);
if (web_contents && (item->IsSavePackageDownload() ||
(web_contents->GetURL() != item->GetOriginalUrl() &&
web_contents->GetURL() != item->GetURL()))) {
auto* security_state_tab_helper =
SecurityStateTabHelper::FromWebContents(web_contents);
if (security_state_tab_helper) {
UMA_HISTOGRAM_ENUMERATION("Security.SecurityLevel.DownloadStarted",
security_state_tab_helper->GetSecurityLevel(),
security_state::SECURITY_LEVEL_COUNT);
}
}
if (web_contents) {
// TODO(crbug.com/40169435): Add test for this metric.
RecordDownloadStartPerProfileType(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
}
// SavePackage downloads are created in a state where they can be shown in the
// browser. Call OnDownloadUpdated() once to notify the UI immediately.
OnDownloadUpdated(manager, item);
}
void DownloadUIController::OnDownloadUpdated(content::DownloadManager* manager,
download::DownloadItem* item) {
DownloadItemModel item_model(item);
bool needs_to_render = false;
#if BUILDFLAG(IS_ANDROID)
if (manager && manager->GetDelegate() &&
manager->GetDelegate()->ShouldOpenPdfInline() &&
!item->IsMustDownload() &&
item->GetState() == download::DownloadItem::IN_PROGRESS &&
base::EqualsCaseInsensitiveASCII(item->GetMimeType(),
pdf::kPDFMimeType)) {
needs_to_render = true;
}
#endif // BUILDFLAG(IS_ANDROID)
// Ignore if we've already notified the UI about |item| or if it isn't a new
// download.
if (item_model.WasUINotified() ||
(!item_model.ShouldNotifyUI() && !needs_to_render)) {
return;
}
// Downloads blocked by local policies should be notified, otherwise users
// won't get any feedback that the download has failed.
bool should_notify =
item->GetLastReason() ==
download::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED &&
item->GetInsecureDownloadStatus() !=
download::DownloadItem::InsecureDownloadStatus::SILENT_BLOCK;
// Wait until the target path is determined or the download is canceled.
if (item->GetTargetFilePath().empty() &&
item->GetState() != download::DownloadItem::CANCELLED && !should_notify) {
return;
}
content::WebContents* web_contents =
content::DownloadItemUtils::GetWebContents(item);
if (web_contents) {
#if BUILDFLAG(IS_ANDROID)
if (!needs_to_render) {
DownloadController::CloseTabIfEmpty(web_contents, item);
}
#else // BUILDFLAG(IS_ANDROID)
Browser* browser = chrome::FindBrowserWithTab(web_contents);
// If the download occurs in a new tab, and it's not a save page
// download (started before initial navigation completed) close it.
// Avoid calling CloseContents if the tab is not in this browser's tab strip
// model; this can happen if the download was initiated by something
// internal to Chrome, such as by the app list.
if (browser && web_contents->GetController().IsInitialNavigation() &&
browser->tab_strip_model()->count() > 1 &&
browser->tab_strip_model()->GetIndexOfWebContents(web_contents) !=
TabStripModel::kNoTab &&
!item->IsSavePackageDownload()) {
web_contents->Close();
}
#endif // BUILDFLAG(IS_ANDROID)
}
if (item->GetState() == download::DownloadItem::CANCELLED)
return;
DownloadItemModel(item).SetWasUINotified(true);
delegate_->OnNewDownloadReady(item);
}