[go: nahoru, domu]

blob: 77145e6f229b43999b6dc8e37b236cb5a3538666 [file] [log] [blame]
// Copyright 2023 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/views/web_apps/isolated_web_apps/isolated_web_app_installer_view_controller.h"
#include <memory>
#include <optional>
#include <string>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/views/web_apps/isolated_web_apps/callback_delayer.h"
#include "chrome/browser/ui/views/web_apps/isolated_web_apps/isolated_web_app_installer_model.h"
#include "chrome/browser/ui/views/web_apps/isolated_web_apps/isolated_web_app_installer_view.h"
#include "chrome/browser/ui/views/web_apps/isolated_web_apps/pref_observer.h"
#include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_metadata.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "components/webapps/common/web_app_id.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "ui/events/event_constants.h"
#else
#include "base/command_line.h"
#endif // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chrome/common/webui_url_constants.h"
#include "chromeos/crosapi/mojom/lacros_shelf_item_tracker.mojom.h"
#include "chromeos/crosapi/mojom/url_handler.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "url/gurl.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace web_app {
namespace {
constexpr base::TimeDelta kGetMetadataMinimumDelay = base::Seconds(2);
constexpr base::TimeDelta kInstallationMinimumDelay = base::Seconds(2);
constexpr double kProgressBarPausePercentage = 0.8;
// A DialogDelegate that notifies callers when it closes.
// Accept/Cancel/Close callbacks could be used together to figure out when a
// dialog closes, but this provides a simpler single callback.
class OnCompleteDialogDelegate : public views::DialogDelegate {
public:
~OnCompleteDialogDelegate() override {
if (callback_) {
std::move(callback_).Run();
}
}
void SetCompleteCallback(base::OnceClosure callback) {
callback_ = std::move(callback);
}
private:
base::OnceClosure callback_;
};
std::string CreateRandomInstanceId() {
base::Uuid uuid = base::Uuid::GenerateRandomV4();
return uuid.AsLowercaseString();
}
} // namespace
struct IsolatedWebAppInstallerViewController::InstallabilityCheckedVisitor {
explicit InstallabilityCheckedVisitor(
IsolatedWebAppInstallerModel& model,
IsolatedWebAppInstallerViewController& controller)
: model_(model), controller_(controller) {}
void operator()(const InstallabilityChecker::BundleInvalid& invalid) {
LOG(ERROR) << "Isolated Web App bundle installability check failed: "
<< invalid.error;
model_->SetDialog(IsolatedWebAppInstallerModel::BundleInvalidDialog{});
controller_->OnModelChanged();
}
void operator()(const InstallabilityChecker::BundleInstallable& installable) {
if (!installable.metadata.icons().empty()) {
// Get the last icon from |any|, size doesn't matter since Shelf will
// rescale the icon anyway.
controller_->SetIcon(gfx::ImageSkia::CreateFrom1xBitmap(
installable.metadata.icons()
.GetBitmapsForPurpose(IconPurpose::ANY)
.rbegin()
->second));
controller_->AddOrUpdateWindowToShelf();
}
model_->SetSignedWebBundleMetadata(installable.metadata);
model_->SetStep(IsolatedWebAppInstallerModel::Step::kShowMetadata);
controller_->OnModelChanged();
}
void operator()(const InstallabilityChecker::BundleUpdatable& updatable) {
// TODO(crbug.com/1479140): Handle updates
controller_->Close();
}
void operator()(const InstallabilityChecker::BundleOutdated& outdated) {
if (outdated.metadata.version() == outdated.installed_version) {
model_->SetDialog(
IsolatedWebAppInstallerModel::BundleAlreadyInstalledDialog{
outdated.metadata.app_name(), outdated.installed_version});
} else {
model_->SetDialog(IsolatedWebAppInstallerModel::BundleOutdatedDialog{
outdated.metadata.app_name(), outdated.metadata.version(),
outdated.installed_version});
}
controller_->OnModelChanged();
}
void operator()(const InstallabilityChecker::ProfileShutdown&) {
controller_->Close();
}
private:
raw_ref<IsolatedWebAppInstallerModel> model_;
raw_ref<IsolatedWebAppInstallerViewController> controller_;
};
IsolatedWebAppInstallerViewController::IsolatedWebAppInstallerViewController(
Profile* profile,
WebAppProvider* web_app_provider,
IsolatedWebAppInstallerModel* model,
std::unique_ptr<IsolatedWebAppsEnabledPrefObserver> pref_observer)
: instance_id_(CreateRandomInstanceId()),
profile_(profile),
web_app_provider_(web_app_provider),
model_(model),
view_(nullptr),
dialog_delegate_(nullptr),
pref_observer_(std::move(pref_observer)) {
CHECK(profile_);
CHECK(model_);
CHECK(web_app_provider_);
CHECK(pref_observer_);
}
IsolatedWebAppInstallerViewController::
~IsolatedWebAppInstallerViewController() = default;
void IsolatedWebAppInstallerViewController::Start(
base::OnceClosure initialized_callback,
base::OnceClosure completion_callback) {
CHECK(initialized_callback);
initialized_callback_ = std::move(initialized_callback);
CHECK(completion_callback);
completion_callback_ = std::move(completion_callback);
// This callback will be posted asynchronously by the |pref_observer_|:
// - Once on `Start()` of `pref_observer_`.
// - Every time the pref value is changed.
IsolatedWebAppsEnabledPrefObserver::PrefChangedCallback
pref_changed_callback = base::BindRepeating(
&IsolatedWebAppInstallerViewController::OnPrefChanged,
weak_ptr_factory_.GetWeakPtr());
pref_observer_->Start(pref_changed_callback);
}
void IsolatedWebAppInstallerViewController::AddOrUpdateWindowToShelf() {
if (!window_) {
return;
}
// Currently only supports Lacros.
// TODO(crbug.com/1515466): Ash Implementation.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
chromeos::LacrosService* lacros_service = chromeos::LacrosService::Get();
if (lacros_service->IsAvailable<crosapi::mojom::LacrosShelfItemTracker>()) {
std::string window_id =
lacros_window_utility::GetRootWindowUniqueId(window_);
crosapi::mojom::WindowDataPtr window_data =
crosapi::mojom::WindowData::New();
window_data->item_id = instance_id_;
window_data->window_id = window_id;
window_data->instance_type =
crosapi::mojom::InstanceType::kIsolatedWebAppInstaller;
window_data->icon = icon_;
lacros_service->GetRemote<crosapi::mojom::LacrosShelfItemTracker>()
->AddOrUpdateWindow(std::move(window_data));
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
}
void IsolatedWebAppInstallerViewController::SetIcon(gfx::ImageSkia icon) {
icon_ = icon;
}
void IsolatedWebAppInstallerViewController::SetViewForTesting(
IsolatedWebAppInstallerView* view) {
view_ = view;
}
void IsolatedWebAppInstallerViewController::Show() {
CHECK(is_initialized_) << "Show() is being called before initialized.";
CHECK(!view_) << "Show() should not be called twice";
auto view = IsolatedWebAppInstallerView::Create(this);
view_ = view.get();
std::unique_ptr<views::DialogDelegate> dialog_delegate =
CreateDialogDelegate(std::move(view));
dialog_delegate_ = dialog_delegate.get();
OnModelChanged();
views::Widget* widget =
views::DialogDelegate::CreateDialogWidget(std::move(dialog_delegate),
/*context=*/nullptr,
/*parent=*/nullptr);
CHECK(!window_);
window_ = widget->GetNativeWindow();
AddOrUpdateWindowToShelf();
widget->Show();
}
void IsolatedWebAppInstallerViewController::FocusWindow() {
if (!window_) {
return;
}
auto* widget = views::Widget::GetWidgetForNativeWindow(window_);
widget->Activate();
}
// static
bool IsolatedWebAppInstallerViewController::OnAcceptWrapper(
base::WeakPtr<IsolatedWebAppInstallerViewController> controller) {
if (controller) {
return controller->OnAccept();
}
return true;
}
// Returns true if the dialog should be closed.
bool IsolatedWebAppInstallerViewController::OnAccept() {
switch (model_->step()) {
case IsolatedWebAppInstallerModel::Step::kShowMetadata: {
model_->SetDialog(IsolatedWebAppInstallerModel::ConfirmInstallationDialog{
base::BindRepeating(&IsolatedWebAppInstallerViewController::
OnShowMetadataLearnMoreClicked,
base::Unretained(this))});
OnModelChanged();
return false;
}
case IsolatedWebAppInstallerModel::Step::kInstallSuccess: {
webapps::AppId app_id = model_->bundle_metadata().app_id();
#if BUILDFLAG(IS_CHROMEOS)
apps::AppServiceProxyFactory::GetForProfile(profile_)->Launch(
app_id, ui::EF_NONE, apps::LaunchSource::kFromInstaller,
/*window_info=*/nullptr);
#else
web_app_provider_->scheduler().LaunchApp(
app_id, *base::CommandLine::ForCurrentProcess(),
/*current_directory=*/base::FilePath(),
/*url_handler_launch_url=*/std::nullopt,
/*protocol_handler_launch_url=*/std::nullopt,
/*file_launch_url=*/std::nullopt, /*launch_files=*/{},
base::DoNothing());
#endif // BUILDFLAG(IS_CHROMEOS)
return true;
}
default:
NOTREACHED();
}
return true;
}
void IsolatedWebAppInstallerViewController::OnComplete() {
view_ = nullptr;
dialog_delegate_ = nullptr;
std::move(completion_callback_).Run();
}
void IsolatedWebAppInstallerViewController::Close() {
if (dialog_delegate_) {
dialog_delegate_->CancelDialog();
}
}
void IsolatedWebAppInstallerViewController::OnPrefChanged(bool enabled) {
if (enabled) {
model_->SetStep(IsolatedWebAppInstallerModel::Step::kGetMetadata);
model_->SetDialog(std::nullopt);
if (!installability_checker_) {
callback_delayer_ = std::make_unique<CallbackDelayer>(
kGetMetadataMinimumDelay, kProgressBarPausePercentage,
base::BindRepeating(&IsolatedWebAppInstallerViewController::
OnGetMetadataProgressUpdated,
weak_ptr_factory_.GetWeakPtr()));
installability_checker_ = InstallabilityChecker::CreateAndStart(
profile_, web_app_provider_, model_->bundle_path(),
callback_delayer_->StartDelayingCallback(base::BindOnce(
&IsolatedWebAppInstallerViewController::OnInstallabilityChecked,
weak_ptr_factory_.GetWeakPtr())));
}
} else {
// Disables the installer if the user has not started installation yet.
// If IWA is disabled after step::kInstall, we allow installation to
// complete and blocks the IWA from launching.
if (model_->step() < IsolatedWebAppInstallerModel::Step::kInstall) {
model_->SetStep(IsolatedWebAppInstallerModel::Step::kDisabled);
model_->SetDialog(std::nullopt);
installability_checker_.reset();
}
}
OnModelChanged();
if (!is_initialized_) {
is_initialized_ = true;
std::move(initialized_callback_).Run();
}
}
void IsolatedWebAppInstallerViewController::OnGetMetadataProgressUpdated(
double progress) {
if (view_) {
view_->UpdateGetMetadataProgress(progress);
}
}
void IsolatedWebAppInstallerViewController::OnInstallabilityChecked(
InstallabilityChecker::Result result) {
absl::visit(InstallabilityCheckedVisitor(*model_, *this), result);
}
void IsolatedWebAppInstallerViewController::OnInstallProgressUpdated(
double progress) {
if (view_) {
view_->UpdateInstallProgress(progress);
}
}
void IsolatedWebAppInstallerViewController::OnInstallComplete(
base::expected<InstallIsolatedWebAppCommandSuccess,
InstallIsolatedWebAppCommandError> result) {
if (result.has_value()) {
model_->SetStep(IsolatedWebAppInstallerModel::Step::kInstallSuccess);
} else {
model_->SetDialog(IsolatedWebAppInstallerModel::InstallationFailedDialog{});
}
OnModelChanged();
}
void IsolatedWebAppInstallerViewController::OnShowMetadataLearnMoreClicked() {
// TODO(crbug.com/1479140): Implement
}
void IsolatedWebAppInstallerViewController::OnSettingsLinkClicked() {
#if BUILDFLAG(IS_CHROMEOS_ASH)
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
profile_, chromeos::settings::mojom::kManageIsolatedWebAppsSubpagePath);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
chromeos::LacrosService* service = chromeos::LacrosService::Get();
DCHECK(service->IsAvailable<crosapi::mojom::UrlHandler>());
GURL manage_isolated_web_apps_subpage_url =
GURL(chrome::kChromeUIOSSettingsURL)
.Resolve(
chromeos::settings::mojom::kManageIsolatedWebAppsSubpagePath);
service->GetRemote<crosapi::mojom::UrlHandler>()->OpenUrl(
manage_isolated_web_apps_subpage_url);
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
}
void IsolatedWebAppInstallerViewController::OnChildDialogCanceled() {
// Currently all child dialogs should close the installer when closed.
Close();
}
void IsolatedWebAppInstallerViewController::OnChildDialogAccepted() {
switch (model_->step()) {
case IsolatedWebAppInstallerModel::Step::kShowMetadata: {
model_->SetStep(IsolatedWebAppInstallerModel::Step::kInstall);
model_->SetDialog(std::nullopt);
OnModelChanged();
callback_delayer_ = std::make_unique<CallbackDelayer>(
kInstallationMinimumDelay, kProgressBarPausePercentage,
base::BindRepeating(
&IsolatedWebAppInstallerViewController::OnInstallProgressUpdated,
weak_ptr_factory_.GetWeakPtr()));
const SignedWebBundleMetadata& metadata = model_->bundle_metadata();
web_app_provider_->scheduler().InstallIsolatedWebApp(
metadata.url_info(), metadata.location(), metadata.version(),
/*optional_keep_alive=*/nullptr,
/*optional_profile_keep_alive=*/nullptr,
callback_delayer_->StartDelayingCallback(base::BindOnce(
&IsolatedWebAppInstallerViewController::OnInstallComplete,
weak_ptr_factory_.GetWeakPtr())));
break;
}
case IsolatedWebAppInstallerModel::Step::kInstall:
// A child dialog on the install screen means the installation failed.
// Accepting the dialog corresponds to the Retry button.
model_->SetDialog(std::nullopt);
installability_checker_.reset();
pref_observer_->Reset();
Start(base::DoNothing(), std::move(completion_callback_));
break;
default:
NOTREACHED();
}
}
void IsolatedWebAppInstallerViewController::OnModelChanged() {
if (!view_) {
return;
}
switch (model_->step()) {
case IsolatedWebAppInstallerModel::Step::kDisabled:
IsolatedWebAppInstallerView::SetDialogButtons(
dialog_delegate_, IDS_APP_CLOSE,
/*accept_button_label_id=*/std::nullopt);
view_->ShowDisabledScreen();
break;
case IsolatedWebAppInstallerModel::Step::kGetMetadata:
IsolatedWebAppInstallerView::SetDialogButtons(
dialog_delegate_, IDS_APP_CANCEL,
/*accept_button_label_id=*/std::nullopt);
view_->ShowGetMetadataScreen();
break;
case IsolatedWebAppInstallerModel::Step::kShowMetadata:
IsolatedWebAppInstallerView::SetDialogButtons(
dialog_delegate_, IDS_APP_CANCEL, IDS_INSTALL);
view_->ShowMetadataScreen(model_->bundle_metadata());
break;
case IsolatedWebAppInstallerModel::Step::kInstall:
IsolatedWebAppInstallerView::SetDialogButtons(
dialog_delegate_, IDS_APP_CANCEL,
/*accept_button_label_id=*/std::nullopt);
view_->ShowInstallScreen(model_->bundle_metadata());
break;
case IsolatedWebAppInstallerModel::Step::kInstallSuccess:
IsolatedWebAppInstallerView::SetDialogButtons(
dialog_delegate_, IDS_IWA_INSTALLER_SUCCESS_FINISH,
IDS_IWA_INSTALLER_SUCCESS_LAUNCH_APPLICATION);
view_->ShowInstallSuccessScreen(model_->bundle_metadata());
break;
}
if (model_->has_dialog()) {
view_->ShowDialog(model_->dialog());
}
}
std::unique_ptr<views::DialogDelegate>
IsolatedWebAppInstallerViewController::CreateDialogDelegate(
std::unique_ptr<views::View> contents_view) {
gfx::Size contents_max_size = contents_view->GetMaximumSize();
auto delegate = std::make_unique<OnCompleteDialogDelegate>();
delegate->set_internal_name(
IsolatedWebAppInstallerView::kInstallerWidgetName);
delegate->SetOwnedByWidget(true);
delegate->SetContentsView(std::move(contents_view));
delegate->SetModalType(ui::MODAL_TYPE_WINDOW);
delegate->SetShowCloseButton(false);
delegate->SetHasWindowSizeControls(false);
delegate->SetCanResize(false);
delegate->set_fixed_width(contents_max_size.width());
// TODO(crbug.com/1479140): Set the title of the dialog for Alt+Tab
delegate->SetShowTitle(false);
delegate->SetAcceptCallbackWithClose(base::BindRepeating(
&IsolatedWebAppInstallerViewController::OnAcceptWrapper,
weak_ptr_factory_.GetWeakPtr()));
delegate->SetCompleteCallback(
base::BindOnce(&IsolatedWebAppInstallerViewController::OnComplete,
weak_ptr_factory_.GetWeakPtr()));
return delegate;
}
} // namespace web_app