blob: e4f9b15fbc83c795c839ee95b5d4a89aca45f7a2 [file] [log] [blame]
// Copyright 2025 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/win/installer_downloader/installer_downloader_model_impl.h"
#include <memory>
#include <utility>
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/win/cloud_synced_folder_checker.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_pref_names.h"
#include "chrome/browser/win/installer_downloader/system_info_provider.h"
#include "chrome/browser/win/installer_downloader/system_info_provider_impl.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/download_manager.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "url/gurl.h"
namespace installer_downloader {
namespace {
// Indicates that the download reason why a download failed.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. If entries are added, kMaxValue
// should be updated. Keep in sync with the
// InstallerDownloaderFailureReason UMA enum defined in
// //tools/metrics/histograms/metadata/windows/enums.xml.
//
// LINT.IfChange(InstallerDownloaderFailureReason)
enum class InstallerDownloaderFailureReason {
kInterrupted = 0,
kCancelled = 1,
kDestroyed = 2,
kMaxValue = kDestroyed,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/windows/enums.xml:InstallerDownloaderFailureReason)
void RecordDownloadFailureReason(InstallerDownloaderFailureReason reason) {
base::UmaHistogramEnumeration("Windows.InstallerDownloader.FailureReason",
reason);
}
} // namespace
class InstallerDownloaderObserver final
: public download::DownloadItem::Observer {
public:
InstallerDownloaderObserver(download::DownloadItem* item,
base::FilePath expected_path,
CompletionCallback completion_callback)
: completion_callback_(std::move(completion_callback)),
expected_path_(expected_path) {
observation_.Observe(item);
}
InstallerDownloaderObserver(const InstallerDownloaderObserver&) = delete;
InstallerDownloaderObserver& operator=(const InstallerDownloaderObserver&) =
delete;
private:
void OnDownloadUpdated(download::DownloadItem* item) override {
CHECK_EQ(observation_.GetSource(), item);
const auto state = item->GetState();
switch (state) {
case download::DownloadItem::COMPLETE:
// Did DownloadManager keep exactly the path we requested?
base::UmaHistogramBoolean(
"Windows.InstallerDownloader.DestinationMatches",
item->GetTargetFilePath() == expected_path_);
// `this` is deleted by `completion_callback_`, so nothing below this
// point may access it.
std::move(completion_callback_).Run(/*succeeded=*/true);
break;
case download::DownloadItem::IN_PROGRESS:
break;
case download::DownloadItem::INTERRUPTED:
case download::DownloadItem::CANCELLED:
RecordDownloadFailureReason(
state == download::DownloadItem::INTERRUPTED
? InstallerDownloaderFailureReason::kInterrupted
: InstallerDownloaderFailureReason::kCancelled);
// `this` is deleted by `completion_callback_`, so nothing below this
// point may access it.
std::move(completion_callback_).Run(/*succeeded=*/false);
break;
case download::DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED();
}
}
void OnDownloadDestroyed(download::DownloadItem* item) override {
CHECK_EQ(observation_.GetSource(), item);
RecordDownloadFailureReason(InstallerDownloaderFailureReason::kDestroyed);
std::move(completion_callback_).Run(/*succeeded=*/false);
}
base::ScopedObservation<download::DownloadItem,
download::DownloadItem::Observer>
observation_{this};
CompletionCallback completion_callback_;
// Stores the expected destination path of the downloaded file.
const base::FilePath expected_path_;
};
InstallerDownloaderModelImpl::InstallerDownloaderModelImpl(
std::unique_ptr<SystemInfoProvider> system_info_provider)
: system_info_provider_(std::move(system_info_provider)) {}
InstallerDownloaderModelImpl::~InstallerDownloaderModelImpl() = default;
void InstallerDownloaderModelImpl::CheckEligibility(
EligibilityCheckCallback callback) {
if (!system_info_provider_->IsOsEligible()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::nullopt));
return;
}
// Installer Downloader is a global feature, therefore it's guaranteed that
// InstallerDownloaderModelImpl will be alive at any point during the browser
// runtime.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&InstallerDownloaderModelImpl::GetInstallerDestination,
base::Unretained(this)),
std::move(callback));
}
void InstallerDownloaderModelImpl::StartDownload(
const GURL& url,
const base::FilePath& destination,
content::DownloadManager& download_manager,
CompletionCallback completion_callback) {
CHECK(url.is_valid());
static constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("windows_installer_downloader",
R"(semantics {
sender: "Windows Installer Downloader"
description:
"Download Chrome installer to the user OneDrive folder on "
"their consent."
trigger: "Once, when the user accept the download request."
data: "None."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
owners: "//chrome/browser/win/installer_downloader/OWNERS"
}
}
last_reviewed: "2025-05-01
}
policy {
cookies_allowed: NO
setting:
"Users can controller this feature by closing the Installer"
"Downloader Infobar."
})");
auto params = std::make_unique<download::DownloadUrlParameters>(
url, kTrafficAnnotation);
params->set_file_path(destination);
params->set_transient(true);
params->set_download_source(download::DownloadSource::INTERNAL_API);
// The InstallerDownloaderController that hold this model is a browser global
// feature. Therefore, it is safe to use base::Unretained here.
params->set_callback(base::BindOnce(
&InstallerDownloaderModelImpl::OnInstallerDownloadCreated,
base::Unretained(this), destination, std::move(completion_callback)));
download_manager.DownloadUrl(std::move(params));
}
bool InstallerDownloaderModelImpl::CanShowInfobar() const {
const PrefService* local_state = g_browser_process->local_state();
if (local_state->GetBoolean(
prefs::kInstallerDownloaderPreventFutureDisplay)) {
return false;
}
return local_state->GetInteger(prefs::kInstallerDownloaderInfobarShowCount) <
kMaxShowCount;
}
void InstallerDownloaderModelImpl::IncrementShowCount() {
PrefService* local_state = g_browser_process->local_state();
local_state->SetInteger(
prefs::kInstallerDownloaderInfobarShowCount,
local_state->GetInteger(prefs::kInstallerDownloaderInfobarShowCount) + 1);
local_state->SetTime(prefs::kInstallerDownloaderInfobarLastShowTime,
base::Time::Now());
}
void InstallerDownloaderModelImpl::PreventFutureDisplay() {
g_browser_process->local_state()->SetBoolean(
prefs::kInstallerDownloaderPreventFutureDisplay, true);
}
bool InstallerDownloaderModelImpl::ShouldByPassEligibilityCheck() const {
return g_browser_process->local_state()->GetBoolean(
prefs::kInstallerDownloaderBypassEligibilityCheck);
}
void InstallerDownloaderModelImpl::OnInstallerDownloadCreated(
const base::FilePath& expected_path,
CompletionCallback completion_callback,
download::DownloadItem* item,
download::DownloadInterruptReason reason) {
CHECK(!installer_downloader_observer_);
// If `reason` is anything other than NONE (or we never got a DownloadItem)
// will be treated as a failure.
if (reason != download::DOWNLOAD_INTERRUPT_REASON_NONE || !item) {
// Records the interrupt reason to a sparse histogram. This is preferred for
// enums with many possible values, where not all may be emitted.
base::UmaHistogramSparse("Windows.InstallerDownloader.InterruptReason",
static_cast<int>(reason));
std::move(completion_callback).Run(/*succeeded=*/false);
return;
}
// The InstallerDownloaderController that hold this model is a browser global
// feature. Therefore, it is safe to use base::Unretained here.
installer_downloader_observer_ =
std::make_unique<InstallerDownloaderObserver>(
item, expected_path,
base::BindOnce(
&InstallerDownloaderModelImpl::OnInstallerDownloadFinished,
base::Unretained(this), std::move(completion_callback)));
}
void InstallerDownloaderModelImpl::OnInstallerDownloadFinished(
CompletionCallback completion_callback,
bool succeeded) {
installer_downloader_observer_.reset();
std::move(completion_callback).Run(succeeded);
}
std::optional<base::FilePath>
InstallerDownloaderModelImpl::GetInstallerDestination() const {
// 1) If this machine already meets the Win‑11 hardware requirements, an
// in‑place upgrade is possible, so there is no need for a standalone
// installer.
if (system_info_provider_->IsHardwareEligibleForWin11()) {
return std::nullopt;
}
// 2) Check OneDrive sync status (root + Desktop).
cloud_synced_folder_checker::CloudSyncStatus cloud_sync_status =
system_info_provider_->EvaluateOneDriveSyncStatus();
// 3) Prefer the syncing Desktop folder; otherwise fall back to OneDrive root.
if (cloud_sync_status.desktop_path.has_value()) {
return std::move(cloud_sync_status.desktop_path);
}
return std::move(cloud_sync_status.one_drive_path);
}
} // namespace installer_downloader