blob: d1022a20f0242bd1bbeb72434256c19c351f627f [file] [log] [blame]
// Copyright 2022 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/bubble/download_bubble_ui_controller.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_index/content_index_provider_impl.h"
#include "chrome/browser/download/bubble/download_bubble_prefs.h"
#include "chrome/browser/download/bubble/download_bubble_update_service.h"
#include "chrome/browser/download/bubble/download_bubble_update_service_factory.h"
#include "chrome/browser/download/bubble/download_bubble_utils.h"
#include "chrome/browser/download/bubble/download_display_controller.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_item_model.h"
#include "chrome/browser/download/download_item_warning_data.h"
#include "chrome/browser/download/download_item_web_app_data.h"
#include "chrome/browser/download/download_ui_model.h"
#include "chrome/browser/download/download_warning_desktop_hats_utils.h"
#include "chrome/browser/download/offline_item_model_manager.h"
#include "chrome/browser/download/offline_item_model_manager_factory.h"
#include "chrome/browser/download/offline_item_utils.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/hats/trust_safety_sentiment_service.h"
#include "chrome/browser/ui/hats/trust_safety_sentiment_service_factory.h"
#include "chrome/browser/ui/views/download/bubble/download_toolbar_ui_controller.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/download_stats.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager.h"
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#endif
namespace {
using DownloadCreationType = ::download::DownloadItem::DownloadCreationType;
using DownloadUIModelPtr = DownloadUIModel::DownloadUIModelPtr;
// Don't show the partial view more than once per 15 seconds, as this pops up
// automatically and may be annoying to the user. The time is reset when the
// user clicks on the button to open the main view.
constexpr base::TimeDelta kShowPartialViewMinInterval = base::Seconds(15);
bool IsForDownload(BrowserWindowInterface* browser,
download::DownloadItem* item) {
Profile* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(item));
// An off-the-record `profile` should match only the off-the-record browsers,
// but a regular `profile` should match both the regular and off-the-record
// browsers.
if (browser->GetProfile() != profile &&
browser->GetProfile()->GetOriginalProfile() != profile) {
return false;
}
if (DownloadItemWebAppData* web_app_data = DownloadItemWebAppData::Get(item);
web_app_data) {
return web_app::AppBrowserController::IsForWebApp(
browser->GetBrowserForMigrationOnly(), web_app_data->id());
} else {
return !web_app::AppBrowserController::IsWebApp(
browser->GetBrowserForMigrationOnly());
}
}
} // namespace
// static
DownloadBubbleUIController* DownloadBubbleUIController::GetForDownload(
download::DownloadItem* item) {
DownloadBubbleUIController* controller = nullptr;
ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
[&](BrowserWindowInterface* browser) {
if (IsForDownload(browser, item) &&
browser->GetFeatures().download_toolbar_ui_controller() &&
browser->GetFeatures()
.download_toolbar_ui_controller()
->bubble_controller()) {
controller = browser->GetFeatures()
.download_toolbar_ui_controller()
->bubble_controller();
return false; // stop iterating
}
return true; // continue iterating
});
return controller;
}
DownloadBubbleUIController::DownloadBubbleUIController(Browser* browser)
: DownloadBubbleUIController(
browser,
DownloadBubbleUpdateServiceFactory::GetForProfile(
browser->profile())) {}
DownloadBubbleUIController::DownloadBubbleUIController(
Browser* browser,
DownloadBubbleUpdateService* update_service)
: browser_(browser),
profile_(browser->profile()),
update_service_(update_service),
offline_manager_(
OfflineItemModelManagerFactory::GetForBrowserContext(profile_)) {
if (MaybeGetDownloadWarningHatsTrigger(
DownloadWarningHatsType::kDownloadBubbleIgnore)) {
delayed_hats_launcher_ =
std::make_unique<DelayedDownloadWarningHatsLauncher>(
profile_, GetIgnoreDownloadBubbleWarningDelay(),
base::BindRepeating(&DownloadBubbleUIController::CompleteHatsPsd,
weak_factory_.GetWeakPtr()));
browser_activity_watcher_ = std::make_unique<BrowserActivityWatcher>(
base::BindRepeating(&DownloadBubbleUIController::OnBrowserActivity,
weak_factory_.GetWeakPtr()));
}
}
DownloadBubbleUIController::~DownloadBubbleUIController() = default;
void DownloadBubbleUIController::HideDownloadUi() {
display_controller_->HideToolbarButton();
}
void DownloadBubbleUIController::HandleButtonPressed() {
display_controller_->HandleButtonPressed();
}
void DownloadBubbleUIController::OnOfflineItemsAdded(
const OfflineContentProvider::OfflineItemList& items) {
display_controller_->OnNewItem(/*show_animation=*/false);
}
void DownloadBubbleUIController::OnDownloadItemAdded(
download::DownloadItem* item,
bool may_show_animation) {
DownloadItemModel model(item);
if (model.ShouldNotifyUI()) {
model.SetActionedOn(false);
}
display_controller_->OnNewItem(may_show_animation &&
model.ShouldShowDownloadStartedAnimation());
}
void DownloadBubbleUIController::OnOfflineItemRemoved(const ContentId& id) {
if (OfflineItemUtils::IsDownload(id)) {
return;
}
offline_manager_->RemoveOfflineItemModelData(id);
display_controller_->OnRemovedItem(id);
}
void DownloadBubbleUIController::OnDownloadItemRemoved(
download::DownloadItem* item) {
std::make_unique<DownloadItemModel>(item)->SetActionedOn(true);
const ContentId& id = OfflineItemUtils::GetContentIdForDownload(item);
display_controller_->OnRemovedItem(id);
}
void DownloadBubbleUIController::OnOfflineItemUpdated(const OfflineItem& item) {
OfflineItemModel model(offline_manager_, item);
bool may_show_details =
model.ShouldShowInBubble() &&
(browser_ == chrome::FindLastActiveWithProfile(profile_.get()));
// Consider dangerous in-progress downloads to be completed.
bool is_done = model.IsDone() ||
(model.GetState() == download::DownloadItem::IN_PROGRESS &&
!IsModelInProgress(&model));
display_controller_->OnUpdatedItem(is_done, may_show_details);
}
void DownloadBubbleUIController::OnDownloadItemUpdated(
download::DownloadItem* item) {
DownloadItemModel model(item);
bool may_show_details =
model.ShouldShowInBubble() &&
(browser_ == chrome::FindLastActiveWithProfile(profile_.get()));
// Consider dangerous in-progress downloads to be completed.
bool is_done = item->IsDone() ||
(item->GetState() == download::DownloadItem::IN_PROGRESS &&
!IsItemInProgress(item));
if (model.IsDangerous()) {
RecordDangerousDownloadShownToUser(item);
}
display_controller_->OnUpdatedItem(is_done, may_show_details);
}
std::vector<DownloadUIModelPtr> DownloadBubbleUIController::GetDownloadUIModels(
bool is_main_view) {
std::vector<DownloadUIModelPtr> all_items;
if (!update_service_->IsInitialized()) {
return all_items;
}
update_service_->GetAllModelsToDisplay(
all_items, GetWebAppIdForBrowser(browser_),
/*force_backfill_download_items=*/true);
std::vector<DownloadUIModelPtr> items_to_return;
for (auto& model : all_items) {
if (!is_main_view && model->WasActionedOn()) {
continue;
}
// Partial view entries are removed if viewed on the main view after
// completion.
if (is_main_view && !IsModelInProgress(model.get())) {
model->SetActionedOn(true);
}
items_to_return.push_back(std::move(model));
}
return items_to_return;
}
std::vector<DownloadUIModelPtr> DownloadBubbleUIController::GetMainView() {
last_partial_view_shown_time_ = std::nullopt;
last_primary_view_was_partial_ = false;
std::vector<DownloadUIModelPtr> list =
GetDownloadUIModels(/*is_main_view=*/true);
base::UmaHistogramCounts100("Download.Bubble.FullViewSize", list.size());
return list;
}
std::vector<DownloadUIModelPtr> DownloadBubbleUIController::GetPartialView() {
base::Time now = base::Time::Now();
if (last_partial_view_shown_time_.has_value() &&
now - *last_partial_view_shown_time_ < kShowPartialViewMinInterval) {
return {};
}
if (!download::IsDownloadBubblePartialViewEnabled(profile_)) {
return {};
}
std::vector<DownloadUIModelPtr> list =
GetDownloadUIModels(/*is_main_view=*/false);
if (!list.empty()) {
last_primary_view_was_partial_ = true;
last_partial_view_shown_time_ = std::make_optional(now);
}
base::UmaHistogramCounts100("Download.Bubble.PartialViewSize", list.size());
return list;
}
void DownloadBubbleUIController::ProcessDownloadButtonPress(
base::WeakPtr<DownloadUIModel> model,
DownloadCommands::Command command,
bool is_main_view) {
if (!model) {
return;
}
download::DownloadItem* item = model->GetDownloadItem();
DownloadCommands commands(model);
base::UmaHistogramEnumeration("Download.Bubble.ProcessedCommand2", command);
DownloadItemWarningData::WarningSurface warning_surface =
is_main_view ? DownloadItemWarningData::WarningSurface::BUBBLE_MAINPAGE
: DownloadItemWarningData::WarningSurface::BUBBLE_SUBPAGE;
DownloadItemWarningData::WarningAction warning_action =
command == DownloadCommands::KEEP
? DownloadItemWarningData::WarningAction::PROCEED
: DownloadItemWarningData::WarningAction::DISCARD;
switch (command) {
case DownloadCommands::KEEP:
case DownloadCommands::DISCARD: {
if (safe_browsing::IsSafeBrowsingSurveysEnabled(*profile_->GetPrefs())) {
TrustSafetySentimentService* trust_safety_sentiment_service =
TrustSafetySentimentServiceFactory::GetForProfile(profile_);
if (trust_safety_sentiment_service) {
trust_safety_sentiment_service->InteractedWithDownloadWarningUI(
warning_surface, warning_action);
}
}
DownloadItemWarningData::AddWarningActionEvent(item, warning_surface,
warning_action);
// Launch a HaTS survey. Note this needs to come before the command is
// executed, as that may change the state of the DownloadItem.
if (item && CanShowDownloadWarningHatsSurvey(item)) {
DownloadWarningHatsType survey_type =
command == DownloadCommands::KEEP
? DownloadWarningHatsType::kDownloadBubbleBypass
: DownloadWarningHatsType::kDownloadBubbleHeed;
auto psd =
DownloadWarningHatsProductSpecificData::Create(survey_type, item);
CompleteHatsPsd(psd);
MaybeLaunchDownloadWarningHatsSurvey(profile_, psd);
}
commands.ExecuteCommand(command);
break;
}
case DownloadCommands::REVIEW:
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
model->ReviewScanningVerdict(
browser_->tab_strip_model()->GetActiveWebContents());
#endif
break;
case DownloadCommands::RETRY:
RetryDownload(model.get(), command);
break;
case DownloadCommands::CANCEL:
model->SetActionedOn(true);
commands.ExecuteCommand(command);
break;
case DownloadCommands::BYPASS_DEEP_SCANNING: {
DownloadItemWarningData::AddWarningActionEvent(
item, warning_surface,
DownloadItemWarningData::WarningAction::PROCEED_DEEP_SCAN);
// Launch a HaTS survey. Note this needs to come before the command is
// executed, as that may change the state of the DownloadItem.
if (item && CanShowDownloadWarningHatsSurvey(item)) {
auto psd = DownloadWarningHatsProductSpecificData::Create(
DownloadWarningHatsType::kDownloadBubbleBypass, item);
CompleteHatsPsd(psd);
MaybeLaunchDownloadWarningHatsSurvey(profile_, psd);
}
commands.ExecuteCommand(command);
break;
}
case DownloadCommands::LEARN_MORE_SCANNING:
case DownloadCommands::LEARN_MORE_DOWNLOAD_BLOCKED:
DownloadItemWarningData::AddWarningActionEvent(
model->GetDownloadItem(), warning_surface,
DownloadItemWarningData::WarningAction::OPEN_LEARN_MORE_LINK);
commands.ExecuteCommand(command);
break;
case DownloadCommands::DEEP_SCAN:
case DownloadCommands::RESUME:
case DownloadCommands::PAUSE:
case DownloadCommands::OPEN_WHEN_COMPLETE:
case DownloadCommands::SHOW_IN_FOLDER:
case DownloadCommands::ALWAYS_OPEN_TYPE:
case DownloadCommands::CANCEL_DEEP_SCAN:
case DownloadCommands::OPEN_SAFE_BROWSING_SETTING:
commands.ExecuteCommand(command);
break;
default:
NOTREACHED() << "Unexpected button pressed on download bubble: "
<< command;
}
}
void DownloadBubbleUIController::RetryDownload(
DownloadUIModel* model,
DownloadCommands::Command command) {
DCHECK(command == DownloadCommands::RETRY);
display_controller_->HideBubble();
content::DownloadManager* download_manager = profile_->GetDownloadManager();
if (!download_manager) {
return;
}
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("download_bubble_retry_download", R"(
semantics {
sender: "The download bubble"
description: "Kick off retrying an interrupted download."
trigger:
"The user selects the retry button for an interrupted download on "
"the downloads bubble."
data: "None"
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"This feature cannot be disabled by settings, but it's only "
"triggered by user request."
policy_exception_justification: "Not implemented."
})");
// Use the last URL in the chain like resumption does.
auto download_url_params = std::make_unique<download::DownloadUrlParameters>(
model->GetURL(), traffic_annotation);
// Set to false because user interaction is needed.
download_url_params->set_content_initiated(false);
download_url_params->set_download_source(
download::DownloadSource::RETRY_FROM_BUBBLE);
download_manager->DownloadUrl(std::move(download_url_params));
}
void DownloadBubbleUIController::ScheduleCancelForEphemeralWarning(
const std::string& guid) {
// Schedule hiding the item from the download bubble.
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DownloadBubbleUpdateService::OnEphemeralWarningExpired,
update_service_->GetWeakPtr(), guid),
DownloadItemModel::kEphemeralWarningLifetimeOnBubble);
// Schedule cancelling the download altogether.
DownloadCoreService* download_core_service =
DownloadCoreServiceFactory::GetForBrowserContext(profile_);
if (!download_core_service) {
return;
}
ChromeDownloadManagerDelegate* delegate =
download_core_service->GetDownloadManagerDelegate();
if (delegate) {
delegate->ScheduleCancelForEphemeralWarning(guid);
}
}
void DownloadBubbleUIController::CompleteHatsPsd(
DownloadWarningHatsProductSpecificData& psd) {
psd.AddPartialViewInteraction(last_primary_view_was_partial());
}
void DownloadBubbleUIController::OnBrowserActivity() {
CHECK(browser_activity_watcher_);
CHECK(delayed_hats_launcher_);
delayed_hats_launcher_->RecordBrowserActivity();
}
void DownloadBubbleUIController::RecordDangerousDownloadShownToUser(
download::DownloadItem* download) {
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(
browser_->profile());
tracker->NotifyEvent("download_bubble_dangerous_download_detected");
// Schedule a survey to be shown if the user ignores the survey for the whole
// delay period, but is otherwise actively using the browser.
if (CanShowDownloadWarningHatsSurvey(download) && delayed_hats_launcher_) {
delayed_hats_launcher_->TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, download);
}
}
base::WeakPtr<DownloadBubbleUIController>
DownloadBubbleUIController::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}