blob: e1178222b8dca9ec0c872f80a4dc6363d6fd7be3 [file] [log] [blame]
// Copyright 2019 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.
#include "chrome/browser/ui/views/web_apps/web_app_uninstall_dialog_view.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/native_window_tracker.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/web_apps/web_app_info_image_source.h"
#include "chrome/browser/web_applications/components/install_finalizer.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "content/public/browser/clear_site_data_utils.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
namespace {
constexpr int kIconSizeInDip = 32;
// The type of action the dialog took at close. Do not reorder this enum as it
// is used in UMA histograms. Any new entries must be added into
// WebappUninstallDialogAction enum in enums.xml file. Matches
// ExtensionUninstallDialog::CloseAction for historical reasons.
enum HistogramCloseAction {
kUninstall = 0,
kUninstallAndCheckboxChecked = 1,
kCancelled = 2,
kMaxValue = kCancelled
};
} // namespace
WebAppUninstallDialogDelegateView::WebAppUninstallDialogDelegateView(
Profile* profile,
WebAppUninstallDialogViews* dialog_view,
web_app::AppId app_id,
webapps::WebappUninstallSource uninstall_source,
std::map<SquareSizePx, SkBitmap> icon_bitmaps)
: dialog_(dialog_view), app_id_(app_id), profile_(profile) {
auto* provider = web_app::WebAppProvider::Get(profile_);
DCHECK(provider);
app_start_url_ = provider->registrar().GetAppStartUrl(app_id_);
DCHECK(!app_start_url_.is_empty());
DCHECK(app_start_url_.is_valid());
gfx::Size image_size{kIconSizeInDip, kIconSizeInDip};
image_ = gfx::ImageSkia(
std::make_unique<WebAppInfoImageSource>(kIconSizeInDip, icon_bitmaps),
image_size);
SetModalType(ui::MODAL_TYPE_WINDOW);
SetShowCloseButton(false);
set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH));
SetShowIcon(true);
SetTitle(l10n_util::GetStringFUTF16(
IDS_EXTENSION_PROMPT_UNINSTALL_TITLE,
base::UTF8ToUTF16(provider->registrar().GetAppShortName(app_id_))));
SetButtonLabel(
ui::DIALOG_BUTTON_OK,
l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_BUTTON));
SetAcceptCallback(
base::BindOnce(&WebAppUninstallDialogDelegateView::OnDialogAccepted,
base::Unretained(this)));
SetCancelCallback(
base::BindOnce(&WebAppUninstallDialogDelegateView::OnDialogCanceled,
base::Unretained(this)));
ChromeLayoutProvider* layout_provider = ChromeLayoutProvider::Get();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
layout_provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_VERTICAL)));
// Add margins for the icon plus the icon-title padding so that the dialog
// contents align with the title text.
gfx::Insets insets = layout_provider->GetDialogInsetsForContentType(
views::DialogContentType::kText, views::DialogContentType::kText);
set_margins(insets + gfx::Insets(0, insets.left() + kIconSizeInDip, 0, 0));
std::u16string checkbox_label = l10n_util::GetStringFUTF16(
IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX,
url_formatter::FormatUrlForSecurityDisplay(
app_start_url_, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC));
auto checkbox = std::make_unique<views::Checkbox>(checkbox_label);
checkbox->SetMultiLine(true);
checkbox_ = AddChildView(std::move(checkbox));
uninstall_source_ = uninstall_source;
chrome::RecordDialogCreation(chrome::DialogIdentifier::EXTENSION_UNINSTALL);
}
WebAppUninstallDialogDelegateView::~WebAppUninstallDialogDelegateView() {
if (dialog_)
dialog_->UninstallCancelled();
}
void WebAppUninstallDialogDelegateView::OnDialogAccepted() {
if (!dialog_)
return;
HistogramCloseAction action =
checkbox_->GetChecked()
? HistogramCloseAction::kUninstallAndCheckboxChecked
: HistogramCloseAction::kUninstall;
UMA_HISTOGRAM_ENUMERATION("Webapp.UninstallDialogAction", action);
Uninstall();
if (checkbox_->GetChecked())
ClearWebAppSiteData();
}
void WebAppUninstallDialogDelegateView::OnDialogCanceled() {
UMA_HISTOGRAM_ENUMERATION("Webapp.UninstallDialogAction",
HistogramCloseAction::kCancelled);
if (dialog_)
std::exchange(dialog_, nullptr)->UninstallCancelled();
}
ui::ImageModel WebAppUninstallDialogDelegateView::GetWindowIcon() {
return ui::ImageModel::FromImageSkia(image_);
}
void WebAppUninstallDialogDelegateView::Uninstall() {
auto* provider = web_app::WebAppProvider::Get(profile_);
DCHECK(provider);
if (!provider->install_finalizer().CanUserUninstallWebApp(app_id_)) {
std::exchange(dialog_, nullptr)->UninstallCancelled();
return;
}
// Forward callback from the WebAppUninstallDialogViews because
// WebAppUninstallDialogDelegateView lifetime is controlled by Widget and it
// is terminiated as soon as dialog is closed regardless of web app
// uninstallation.
provider->install_finalizer().UninstallWebApp(app_id_, uninstall_source_,
dialog_->UninstallStarted());
// We successfully call Web App Uninstall routine, then
// WebAppUninstallDialogDelegateView can be terminated, but can't call the
// callback of the dialog caller.
dialog_ = nullptr;
}
void WebAppUninstallDialogDelegateView::ClearWebAppSiteData() {
content::ClearSiteData(
base::BindRepeating(
[](content::BrowserContext* browser_context) {
return browser_context;
},
base::Unretained(profile_)),
url::Origin::Create(app_start_url_), /*clear_cookies=*/true,
/*clear_storage=*/true, /*clear_cache=*/true,
/*avoid_closing_connections=*/false, base::DoNothing());
}
void WebAppUninstallDialogDelegateView::ProcessAutoConfirmValue() {
switch (extensions::ScopedTestDialogAutoConfirm::GetAutoConfirmValue()) {
case extensions::ScopedTestDialogAutoConfirm::NONE:
break;
case extensions::ScopedTestDialogAutoConfirm::ACCEPT_AND_OPTION:
case extensions::ScopedTestDialogAutoConfirm::ACCEPT_AND_REMEMBER_OPTION:
checkbox_->SetChecked(/*checked=*/true);
AcceptDialog();
break;
case extensions::ScopedTestDialogAutoConfirm::ACCEPT:
AcceptDialog();
break;
case extensions::ScopedTestDialogAutoConfirm::CANCEL:
CancelDialog();
break;
}
}
BEGIN_METADATA(WebAppUninstallDialogDelegateView, views::DialogDelegateView)
END_METADATA
WebAppUninstallDialogViews::WebAppUninstallDialogViews(Profile* profile,
gfx::NativeWindow parent)
: parent_(parent), profile_(profile) {
if (parent)
parent_window_tracker_ = NativeWindowTracker::Create(parent);
}
WebAppUninstallDialogViews::~WebAppUninstallDialogViews() {
if (view_)
view_->CancelDialog();
}
void WebAppUninstallDialogViews::ConfirmUninstall(
const web_app::AppId& app_id,
webapps::WebappUninstallSource uninstall_source,
WebAppUninstallDialogViews::OnWebAppUninstallDialogClosed closed_callback) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
app_id_ = app_id;
closed_callback_ = std::move(closed_callback);
if (parent_ && parent_window_tracker_->WasNativeWindowClosed()) {
UninstallCancelled();
return;
}
auto* provider = web_app::WebAppProvider::Get(profile_);
DCHECK(provider);
registrar_observation_.Observe(&provider->registrar());
provider->icon_manager().ReadIcons(
app_id, IconPurpose::ANY,
provider->registrar().GetAppDownloadedIconSizesAny(app_id),
base::BindOnce(&WebAppUninstallDialogViews::OnIconsRead,
weak_ptr_factory_.GetWeakPtr(), uninstall_source));
}
void WebAppUninstallDialogViews::SetDialogShownCallbackForTesting(
base::OnceClosure callback) {
dialog_shown_callback_for_testing_ = std::move(callback);
}
void WebAppUninstallDialogViews::OnIconsRead(
webapps::WebappUninstallSource uninstall_source,
std::map<SquareSizePx, SkBitmap> icon_bitmaps) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (parent_ && parent_window_tracker_->WasNativeWindowClosed()) {
UninstallCancelled();
return;
}
view_ = new WebAppUninstallDialogDelegateView(
profile_, this, app_id_, uninstall_source, std::move(icon_bitmaps));
constrained_window::CreateBrowserModalDialogViews(view_, parent_)->Show();
if (dialog_shown_callback_for_testing_)
std::move(dialog_shown_callback_for_testing_).Run();
// This should be a tail call because it destroys |this|:
view_->ProcessAutoConfirmValue();
}
void WebAppUninstallDialogViews::OnWebAppWillBeUninstalled(
const web_app::AppId& app_id) {
// Handle the case when web app was uninstalled externally and we have to
// cancel current dialog.
if (app_id == app_id_ && view_)
view_->CancelDialog();
}
void WebAppUninstallDialogViews::OnAppRegistrarDestroyed() {
registrar_observation_.Reset();
if (view_)
view_->CancelDialog();
}
base::OnceCallback<void(bool uninstalled)>
WebAppUninstallDialogViews::UninstallStarted() {
DCHECK(closed_callback_);
// Next OnWebAppWillBeUninstalled should be ignored. Unsubscribe:
registrar_observation_.Reset();
// The view can now be destroyed without us knowing, so clear it to prevent
// UAF in the destructor.
view_ = nullptr;
return std::move(closed_callback_);
}
void WebAppUninstallDialogViews::UninstallCancelled() {
DCHECK(closed_callback_);
view_ = nullptr;
std::move(closed_callback_).Run(/*uninstalled=*/false);
}
// static
std::unique_ptr<web_app::WebAppUninstallDialog>
web_app::WebAppUninstallDialog::Create(Profile* profile,
gfx::NativeWindow parent) {
return std::make_unique<WebAppUninstallDialogViews>(profile, parent);
}