blob: fe13787292c40bb3c7ff4fa5e5de65f62d7047e1 [file] [log] [blame]
// Copyright 2024 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/ui/extensions/mv2_disabled_dialog_controller.h"
#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/manifest_v2_experiment_manager.h"
#include "chrome/browser/extensions/mv2_experiment_stage.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/extensions/extensions_dialogs.h"
#include "chrome/browser/ui/startup/startup_browser_creator.h"
#include "extensions/browser/extension_icon_placeholder.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/image_loader.h"
#include "extensions/browser/pref_types.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "mojo/public/cpp/bindings/lib/string_serialization.h"
namespace extensions {
namespace {
// Stores a bit for whether the user acknowledged the dialog informing the
// extension being disabled durint the MV2 deprecation 'disable with re-enable'
// experiment stage.
constexpr PrefMap kMV2DeprecationDisabledDialogAcknowledgedPref = {
"mv2_deprecation_disabled_dialog_ack", PrefType::kBool,
PrefScope::kExtensionSpecific};
// Stores a bit for whether the user acknowledged the dialog informing the
// extension being disabled during the MV2 deprecation 'unsupported' experiment
// stage.
constexpr PrefMap kMV2DeprecationUnsupportedDisabledDialogAcknowledgedPref = {
"mv2_deprecation_unsupported_disabled_dialog_ack", PrefType::kBool,
PrefScope::kExtensionSpecific};
// Returns the pref that stores whether the user has acknowledged the MV2
// deprecation disabled dialog for a given extension in `experiment_stage`.
const PrefMap& GetDisabledDialogAcknowledgedPref(
MV2ExperimentStage experiment_stage) {
switch (experiment_stage) {
case MV2ExperimentStage::kWarning:
// There is no disabled dialog for this stage, thus extension cannot be
// acknowledged.
NOTREACHED();
case MV2ExperimentStage::kDisableWithReEnable:
return kMV2DeprecationDisabledDialogAcknowledgedPref;
case MV2ExperimentStage::kUnsupported:
return kMV2DeprecationUnsupportedDisabledDialogAcknowledgedPref;
}
}
// Returns whether `extension` should be included in the disabled dialog.
bool IsExtensionAffected(const Extension& extension,
ExtensionPrefs* extension_prefs,
ManagementPolicy* policy,
const PrefMap& dialog_ack_pref) {
// Exclude extensions that are not disabled due to the MV2 deprecation.
if (!extension_prefs->HasDisableReason(
extension.id(),
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION)) {
return false;
}
// Exclude extensions that cannot be uninstalled.
if (policy->MustRemainInstalled(&extension, nullptr) ||
!policy->UserMayModifySettings(&extension, nullptr)) {
return false;
}
// Exclude extensions that were already acknowledged on a previous disabled
// dialog.
bool was_acknowledged = false;
extension_prefs->ReadPrefAsBoolean(extension.id(), dialog_ack_pref,
&was_acknowledged);
return !was_acknowledged;
}
} // namespace
Mv2DisabledDialogController::Mv2DisabledDialogController(Browser* browser)
: browser_(browser) {
ManifestV2ExperimentManager* experiment_manager_ =
ManifestV2ExperimentManager::Get(browser_->profile());
CHECK(experiment_manager_);
experiment_stage_ = experiment_manager_->GetCurrentExperimentStage();
// Dialog should only be visible once.
if (experiment_manager_->has_triggered_disabled_dialog()) {
return;
}
experiment_manager_->SetHasTriggeredDisabledDialog(true);
if (experiment_manager_->is_manager_ready()) {
ComputeAffectedExtensions();
} else {
show_dialog_subscription_ =
experiment_manager_->RegisterOnManagerReadyCallback(base::BindRepeating(
&Mv2DisabledDialogController::ComputeAffectedExtensions,
weak_ptr_factory_.GetWeakPtr()));
}
}
Mv2DisabledDialogController::~Mv2DisabledDialogController() = default;
void Mv2DisabledDialogController::TearDown() {
show_dialog_subscription_ = base::CallbackListSubscription();
}
void Mv2DisabledDialogController::MaybeShowDisabledDialogForTesting() {
ComputeAffectedExtensions();
}
void Mv2DisabledDialogController::ComputeAffectedExtensions() {
auto* extension_registry = ExtensionRegistry::Get(browser_->profile());
auto* extension_prefs = ExtensionPrefs::Get(browser_->profile());
ManagementPolicy* policy =
ExtensionSystem::Get(browser_->profile())->management_policy();
const PrefMap& dialog_ack_pref =
GetDisabledDialogAcknowledgedPref(experiment_stage_);
std::vector<const Extension*> affected_extensions;
for (const scoped_refptr<const Extension>& extension :
extension_registry->disabled_extensions()) {
if (IsExtensionAffected(*extension, extension_prefs, policy,
dialog_ack_pref)) {
affected_extensions.push_back(extension.get());
}
}
// No extensions to show, do nothing.
if (affected_extensions.empty()) {
return;
}
// Retrieve the extension icon for all the affected extensions.
// MaybeShowDisabledDialog will be called once icon is loaded for every
// extension.
base::RepeatingClosure barrier_closure = base::BarrierClosure(
affected_extensions.size(),
base::BindOnce(&Mv2DisabledDialogController::MaybeShowDisabledDialog,
weak_ptr_factory_.GetWeakPtr()));
const int icon_size = affected_extensions.size() == 1
? extension_misc::EXTENSION_ICON_SMALL
: extension_misc::EXTENSION_ICON_SMALLISH;
auto* image_loader = ImageLoader::Get(browser_->profile());
for (const Extension* extension : affected_extensions) {
ExtensionResource icon = IconsInfo::GetIconResource(
extension, icon_size, ExtensionIconSet::Match::kBigger);
if (icon.empty()) {
gfx::Image placeholder_icon =
ExtensionIconPlaceholder::CreateImage(icon_size, extension->name());
OnExtensionIconLoaded(extension->id(), extension->name(), barrier_closure,
placeholder_icon);
} else {
gfx::Size max_size(icon_size, icon_size);
image_loader->LoadImageAsync(
extension, icon, max_size,
base::BindOnce(&Mv2DisabledDialogController::OnExtensionIconLoaded,
weak_ptr_factory_.GetWeakPtr(), extension->id(),
extension->name(), barrier_closure));
}
}
}
void Mv2DisabledDialogController::OnExtensionIconLoaded(
const ExtensionId& extension_id,
const std::string& extension_name,
base::OnceClosure done_callback,
const gfx::Image& icon) {
ExtensionInfo extension_info;
extension_info.id = extension_id;
extension_info.name = extension_name;
extension_info.icon = icon;
affected_extensions_info_.push_back(extension_info);
std::move(done_callback).Run();
}
void Mv2DisabledDialogController::MaybeShowDisabledDialog() {
if (!browser_->window()) {
return;
}
// Extensions can be updated while this call happens. Only include extensions
// that are still affected.
auto* extension_registry = ExtensionRegistry::Get(browser_->profile());
auto* extension_prefs = ExtensionPrefs::Get(browser_->profile());
const PrefMap& dialog_ack_pref =
GetDisabledDialogAcknowledgedPref(experiment_stage_);
ManagementPolicy* policy =
ExtensionSystem::Get(browser_->profile())->management_policy();
affected_extensions_info_.erase(
std::remove_if(affected_extensions_info_.begin(),
affected_extensions_info_.end(),
[&](const ExtensionInfo& extension_info) {
const Extension* extension =
extension_registry->disabled_extensions().GetByID(
extension_info.id);
return !extension ||
!IsExtensionAffected(*extension, extension_prefs,
policy, dialog_ack_pref);
}),
affected_extensions_info_.end());
// No extensions to show, do nothing.
if (affected_extensions_info_.empty()) {
return;
}
// Sort extensions alphabetically.
std::sort(affected_extensions_info_.begin(), affected_extensions_info_.end(),
[](const ExtensionInfo& a, const ExtensionInfo& b) {
return base::ToLowerASCII(a.name) < base::ToLowerASCII(b.name);
});
ShowMv2DeprecationDisabledDialog(
browser_, affected_extensions_info_,
base::BindOnce(&Mv2DisabledDialogController::OnRemoveSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&Mv2DisabledDialogController::OnManageSelected,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&Mv2DisabledDialogController::UserAcknowledgedDialog,
weak_ptr_factory_.GetWeakPtr()));
}
void Mv2DisabledDialogController::OnRemoveSelected() {
CHECK(!affected_extensions_info_.empty());
UserAcknowledgedDialog();
auto* extension_registry = ExtensionRegistry::Get(browser_->profile());
for (const auto& extension_info : affected_extensions_info_) {
const Extension* current_extension = extension_registry->GetExtensionById(
extension_info.id, ExtensionRegistry::EVERYTHING);
// Extensions can be uninstalled externally while the dialog is open. Only
// uninstall extensions that are still existent.
if (!current_extension) {
continue;
}
// If an extension fails to be uninstalled, it will not pause the
// uninstall of the other extensions on the list.
ExtensionRegistrar::Get(browser_->profile())
->UninstallExtension(extension_info.id, UNINSTALL_REASON_USER_INITIATED,
nullptr);
}
if (experiment_stage_ == MV2ExperimentStage::kDisableWithReEnable) {
base::RecordAction(base::UserMetricsAction(
"Extensions.Mv2Deprecation.Disabled.RemoveExtension.DisabledDialog"));
} else {
CHECK_EQ(experiment_stage_, MV2ExperimentStage::kUnsupported);
base::RecordAction(
base::UserMetricsAction("Extensions.Mv2Deprecation.Unsupported."
"RemoveExtension.DisabledDialog"));
}
}
void Mv2DisabledDialogController::OnManageSelected() {
CHECK(!affected_extensions_info_.empty());
UserAcknowledgedDialog();
chrome::ShowExtensions(browser_);
}
void Mv2DisabledDialogController::UserAcknowledgedDialog() {
CHECK(!affected_extensions_info_.empty());
// Store the extensions included in the dialog, so we don't show the dialog
// for them again during the current experiment stage.
auto* extension_prefs = ExtensionPrefs::Get(browser_->profile());
const PrefMap& dialog_ack_pref =
GetDisabledDialogAcknowledgedPref(experiment_stage_);
for (const auto& extension_info : affected_extensions_info_) {
extension_prefs->SetBooleanPref(extension_info.id, dialog_ack_pref, true);
}
if (experiment_stage_ == MV2ExperimentStage::kDisableWithReEnable) {
base::RecordAction(base::UserMetricsAction(
"Extensions.Mv2Deprecation.Disabled.ManageExtension.DisabledDialog"));
} else {
CHECK_EQ(experiment_stage_, MV2ExperimentStage::kUnsupported);
base::RecordAction(
base::UserMetricsAction("Extensions.Mv2Deprecation.Unsupported."
"ManageExtension.DisabledDialog"));
}
}
} // namespace extensions