blob: 6e43fb19bd342b6435c0c3803849d6b4bb894c0c [file] [log] [blame]
// Copyright 2020 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/borealis/borealis_installer_view.h"
#include <memory>
#include "ash/public/cpp/ash_typography.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/style/color_mode_observer.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/public/cpp/window_properties.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/borealis/borealis_app_launcher.h"
#include "chrome/browser/ash/borealis/borealis_context_manager.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_installer.h"
#include "chrome/browser/ash/borealis/borealis_metrics.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/views/borealis/borealis_installer_disallowed_dialog.h"
#include "chrome/browser/ui/views/borealis/borealis_installer_error_dialog.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/view_class_properties.h"
namespace {
BorealisInstallerView* g_borealis_installer_view = nullptr;
constexpr auto kButtonRowInsets = gfx::Insets::TLBR(0, 64, 32, 64);
void ShowBorealisInstallerViewIfAllowed(
Profile* profile,
borealis::BorealisFeatures::AllowStatus status) {
if (status != borealis::BorealisFeatures::AllowStatus::kAllowed) {
views::borealis::ShowInstallerDisallowedDialog(status);
return;
}
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!g_borealis_installer_view) {
g_borealis_installer_view = new BorealisInstallerView(profile);
views::DialogDelegate::CreateDialogWidget(g_borealis_installer_view,
nullptr, nullptr);
g_borealis_installer_view->GetWidget()->GetNativeWindow()->SetProperty(
ash::kShelfIDKey, ash::ShelfID(borealis::kInstallerAppId).Serialize());
}
g_borealis_installer_view->SetButtonRowInsets(kButtonRowInsets);
g_borealis_installer_view->GetWidget()->Show();
}
// Returns the text to be used for a predicted completion time, which is
// something like "starting up" or "3 mins remaining" depending on how
// accurately we can predict.
std::u16string GetInstallationPredictionText(const base::Time& start_time,
double completion_proportion) {
base::TimeDelta duration = base::Time::Now() - start_time;
// We have no confidence in the prediction for the first second or for
// too-small proportions.
if (completion_proportion < 0.001 || duration < base::Seconds(1)) {
return l10n_util::GetStringUTF16(IDS_BOREALIS_INSTALLER_ONGOING_INACTIVE);
}
// Linear-interpolation to predict remaining time.
base::TimeDelta remaining = (duration / completion_proportion) - duration;
return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
ui::TimeFormat::LENGTH_SHORT, remaining);
}
} // namespace
// Defined in chrome/browser/ash/borealis/borealis_util.h.
void borealis::ShowBorealisInstallerView(Profile* profile) {
borealis::BorealisService::GetForProfile(profile)->Features().IsAllowed(
base::BindOnce(&ShowBorealisInstallerViewIfAllowed, profile));
}
// We need a separate class so that we can alert screen readers appropriately
// when the text changes.
class BorealisInstallerView::TitleLabel : public views::Label {
public:
using Label::Label;
METADATA_HEADER(TitleLabel);
TitleLabel() = default;
~TitleLabel() override = default;
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
node_data->role = ax::mojom::Role::kStatus;
node_data->SetNameChecked(GetText());
}
};
BEGIN_METADATA(BorealisInstallerView, TitleLabel, views::Label)
END_METADATA
BorealisInstallerView::BorealisInstallerView(Profile* profile)
: app_name_(l10n_util::GetStringUTF16(IDS_BOREALIS_APP_NAME)),
profile_(profile),
observation_(this) {
SetCanMinimize(true);
set_draggable(true);
set_margins(gfx::Insets::TLBR(80, 40, 40, 40));
set_corner_radius(12);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
views::View* left_container_view =
AddChildView(std::make_unique<views::View>());
views::BoxLayout* left_layout =
left_container_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets()));
left_layout->set_inside_border_insets(gfx::Insets::TLBR(0, 0, 0, 80));
AddChildView(left_container_view);
views::ImageView* flair =
left_container_view->AddChildView(std::make_unique<views::ImageView>());
gfx::ImageSkia* s = ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_LOGO_BOREALIS_FLAIR);
flair->SetHorizontalAlignment(views::ImageView::Alignment::kLeading);
flair->SetImageSize({32, 32});
flair->SetImage(s);
primary_message_label_ =
new TitleLabel(GetPrimaryMessage(), ash::CONTEXT_HEADLINE_OVERSIZED,
views::style::STYLE_PRIMARY);
primary_message_label_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(40, 0, 0, 0));
primary_message_label_->SetMultiLine(true);
primary_message_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
primary_message_label_->SetMaximumWidth(264);
left_container_view->AddChildView(primary_message_label_.get());
secondary_message_label_ = new views::Label(
GetSecondaryMessage(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY);
secondary_message_label_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(16, 0, 0, 0));
secondary_message_label_->SetMultiLine(true);
secondary_message_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
left_container_view->AddChildView(secondary_message_label_.get());
views::View* progress_container =
left_container_view->AddChildView(std::make_unique<views::View>());
progress_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::TLBR(32, 0, 0, 0)));
installation_progress_percentage_label_ =
progress_container->AddChildView(std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY));
installation_progress_separator_ =
progress_container->AddChildView(std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY));
installation_progress_separator_->SetText(u"|");
installation_progress_separator_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(0, 8, 0, 8));
installation_progress_eta_label_ =
progress_container->AddChildView(std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY));
progress_bar_ = new views::ProgressBar();
progress_bar_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(16, 0, 0, 0));
left_container_view->AddChildView(progress_bar_.get());
views::View* right_container_view =
AddChildView(std::make_unique<views::View>());
right_container_layout_ =
right_container_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
right_container_layout_->set_inside_border_insets(
gfx::Insets::TLBR(64, 0, 64, 0));
AddChildView(right_container_view);
big_image_ = new views::ImageView();
big_image_->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
big_image_->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
right_container_view->AddChildView(big_image_.get());
ash::DarkLightModeController* dark_light_controller =
ash::DarkLightModeController::Get();
if (dark_light_controller) {
dark_light_controller->AddObserver(this);
}
}
BorealisInstallerView::~BorealisInstallerView() {
borealis::BorealisInstaller& installer =
borealis::BorealisService::GetForProfile(profile_)->Installer();
if (state_ == State::kConfirmInstall || state_ == State::kInstalling) {
installer.Cancel();
}
ash::DarkLightModeController* dark_light_controller =
ash::DarkLightModeController::Get();
if (dark_light_controller) {
dark_light_controller->RemoveObserver(this);
}
g_borealis_installer_view = nullptr;
}
// static
BorealisInstallerView* BorealisInstallerView::GetActiveViewForTesting() {
return g_borealis_installer_view;
}
bool BorealisInstallerView::ShouldShowCloseButton() const {
return true;
}
bool BorealisInstallerView::ShouldShowWindowTitle() const {
return false;
}
bool BorealisInstallerView::Accept() {
if (state_ == State::kConfirmInstall) {
StartInstallation();
return false;
}
if (state_ == State::kCompleted) {
// Launch button has been clicked.
borealis::BorealisService::GetForProfile(profile_)->AppLauncher().Launch(
borealis::kClientAppId,
base::BindOnce([](borealis::BorealisAppLauncher::LaunchResult result) {
if (result == borealis::BorealisAppLauncher::LaunchResult::kSuccess) {
return;
}
LOG(ERROR) << "Failed to launch borealis after install: code="
<< static_cast<int>(result);
}));
return true;
}
// Retry button has been clicked to retry setting of Borealis environment
// after error occurred.
StartInstallation();
return false;
}
bool BorealisInstallerView::Cancel() {
if (state_ == State::kCompleted) {
borealis::BorealisService::GetForProfile(profile_)
->ContextManager()
.ShutDownBorealis(
base::BindOnce([](borealis::BorealisShutdownResult result) {
if (result == borealis::BorealisShutdownResult::kSuccess) {
return;
}
LOG(ERROR) << "Failed to shutdown borealis after install: code="
<< static_cast<int>(result);
}));
}
return true;
}
void BorealisInstallerView::OnStateUpdated(InstallingState new_state) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(state_, State::kInstalling);
DCHECK_NE(new_state, InstallingState::kInactive);
installing_state_ = new_state;
OnStateUpdated();
}
void BorealisInstallerView::OnProgressUpdated(double fraction_complete) {
progress_bar_->SetValue(fraction_complete);
SetProgressMessageLabel();
}
void BorealisInstallerView::OnInstallationEnded(
borealis::BorealisInstallResult result,
const std::string& error_description) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (result == borealis::BorealisInstallResult::kSuccess) {
state_ = State::kCompleted;
} else if (result != borealis::BorealisInstallResult::kCancelled) {
result_ = result;
LOG(ERROR) << "Borealis Installation Error: " << error_description;
views::borealis::ShowInstallerErrorDialog(
GetWidget()->GetNativeView(), result,
base::BindOnce(&BorealisInstallerView::OnErrorDialogDismissed,
weak_factory_.GetWeakPtr()));
}
installing_state_ = InstallingState::kInactive;
OnStateUpdated();
}
std::u16string BorealisInstallerView::GetPrimaryMessage() const {
switch (state_) {
case State::kConfirmInstall:
return l10n_util::GetStringUTF16(
IDS_BOREALIS_INSTALLER_CONFIRMATION_TITLE);
case State::kInstalling:
return l10n_util::GetStringUTF16(IDS_BOREALIS_INSTALLER_ONGOING_TITLE);
case State::kCompleted:
return l10n_util::GetStringUTF16(IDS_BOREALIS_INSTALLER_FINISHED_TITLE);
}
}
std::u16string BorealisInstallerView::GetSecondaryMessage() const {
switch (state_) {
case State::kConfirmInstall:
return l10n_util::GetStringUTF16(
IDS_BOREALIS_INSTALLER_CONFIRMATION_MESSAGE);
case State::kInstalling:
return l10n_util::GetStringUTF16(IDS_BOREALIS_INSTALLER_ONGOING_MESSAGE);
case State::kCompleted:
return l10n_util::GetStringUTF16(IDS_BOREALIS_INSTALLER_FINISHED_MESSAGE);
}
}
void BorealisInstallerView::SetInstallingStateForTesting(
InstallingState new_state) {
installing_state_ = new_state;
}
int BorealisInstallerView::GetCurrentDialogButtons() const {
switch (state_) {
case State::kInstalling:
return ui::DIALOG_BUTTON_CANCEL;
case State::kConfirmInstall:
case State::kCompleted:
return ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK;
}
}
std::u16string BorealisInstallerView::GetCurrentDialogButtonLabel(
ui::DialogButton button) const {
switch (state_) {
case State::kConfirmInstall:
return l10n_util::GetStringUTF16(
button == ui::DIALOG_BUTTON_OK ? IDS_BOREALIS_INSTALLER_INSTALL_BUTTON
: IDS_APP_CANCEL);
case State::kInstalling:
DCHECK_EQ(button, ui::DIALOG_BUTTON_CANCEL);
return l10n_util::GetStringUTF16(IDS_APP_CANCEL);
case State::kCompleted: {
return l10n_util::GetStringUTF16(
button == ui::DIALOG_BUTTON_OK ? IDS_BOREALIS_INSTALLER_LAUNCH_BUTTON
: IDS_APP_CLOSE);
}
}
}
void BorealisInstallerView::OnErrorDialogDismissed(
views::borealis::ErrorDialogChoice choice) {
switch (choice) {
case views::borealis::ErrorDialogChoice::kRetry:
StartInstallation();
return;
case views::borealis::ErrorDialogChoice::kExit:
GetWidget()->Close();
return;
}
}
void BorealisInstallerView::OnStateUpdated() {
SetPrimaryMessageLabel();
SetSecondaryMessageLabel();
SetProgressMessageLabel();
SetImage();
// todo(danielng): ensure button labels meet a11y requirements.
int buttons = GetCurrentDialogButtons();
SetButtons(buttons);
if (buttons & ui::DIALOG_BUTTON_OK) {
SetButtonLabel(ui::DIALOG_BUTTON_OK,
GetCurrentDialogButtonLabel(ui::DIALOG_BUTTON_OK));
SetDefaultButton(ui::DIALOG_BUTTON_OK);
} else {
SetDefaultButton(ui::DIALOG_BUTTON_NONE);
}
if (buttons & ui::DIALOG_BUTTON_CANCEL) {
SetButtonLabel(ui::DIALOG_BUTTON_CANCEL,
GetCurrentDialogButtonLabel(ui::DIALOG_BUTTON_CANCEL));
}
const bool progress_bar_visible = state_ == State::kInstalling;
progress_bar_->SetVisible(progress_bar_visible);
DialogModelChanged();
primary_message_label_->NotifyAccessibilityEvent(
ax::mojom::Event::kLiveRegionChanged,
/* send_native_event = */ true);
}
void BorealisInstallerView::AddedToWidget() {
// At this point GetWidget() is guaranteed to return non-null.
OnStateUpdated();
}
void BorealisInstallerView::OnColorModeChanged(bool dark_mode_enabled) {
// We check dark-mode ourselves, so no need to propagate the param.
OnStateUpdated();
}
void BorealisInstallerView::SetPrimaryMessageLabel() {
primary_message_label_->SetText(GetPrimaryMessage());
primary_message_label_->SetVisible(true);
primary_message_label_->NotifyAccessibilityEvent(
ax::mojom::Event::kTextChanged, true);
}
void BorealisInstallerView::SetSecondaryMessageLabel() {
secondary_message_label_->SetText(GetSecondaryMessage());
secondary_message_label_->SetVisible(true);
secondary_message_label_->NotifyAccessibilityEvent(
ax::mojom::Event::kTextChanged, true);
}
void BorealisInstallerView::SetProgressMessageLabel() {
bool in_progress = state_ == State::kInstalling;
installation_progress_percentage_label_->SetVisible(in_progress);
installation_progress_separator_->SetVisible(in_progress);
installation_progress_eta_label_->SetVisible(in_progress);
if (!in_progress) {
return;
}
int percentage = progress_bar_->GetValue() * 100;
installation_progress_percentage_label_->SetText(
l10n_util::GetStringFUTF16Int(IDS_BOREALIS_INSTALLER_ONGOING_PERCENTAGE,
percentage));
installation_progress_percentage_label_->SetVisible(true);
installation_progress_percentage_label_->NotifyAccessibilityEvent(
ax::mojom::Event::kTextChanged, true);
installation_progress_eta_label_->SetText(GetInstallationPredictionText(
install_start_time_, progress_bar_->GetValue()));
installation_progress_eta_label_->SetVisible(true);
}
void BorealisInstallerView::SetImage() {
// These values are adjusted from the mocks in b/246659720, to account for
// differences in image resolution.
constexpr int kStartBottomInsetDp = 70;
constexpr int kCompleteBottomInsetDp = 64;
auto set_image = [this](int image_id, int bottom_inset_dp) {
gfx::ImageSkia* s =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(image_id);
// The image assets are sized so that we can display them at half their
// resolution in DP.
big_image_->SetImageSize({s->width() / 2, s->height() / 2});
big_image_->SetImage(s);
};
ash::DarkLightModeController* dark_light_mode_controller =
ash::DarkLightModeController::Get();
bool dark_mode = dark_light_mode_controller &&
dark_light_mode_controller->IsDarkModeEnabled();
if (state_ == State::kCompleted) {
set_image(dark_mode ? IDR_BOREALIS_INSTALLER_COMPLETE_DARK
: IDR_BOREALIS_INSTALLER_COMPLETE_LIGHT,
kCompleteBottomInsetDp);
return;
}
set_image(dark_mode ? IDR_BOREALIS_INSTALLER_START_DARK
: IDR_BOREALIS_INSTALLER_START_LIGHT,
kStartBottomInsetDp);
}
void BorealisInstallerView::StartInstallation() {
state_ = State::kInstalling;
progress_bar_->SetValue(0);
borealis::BorealisInstaller& installer =
borealis::BorealisService::GetForProfile(profile_)->Installer();
if (observation_.IsObserving()) {
observation_.Reset();
}
observation_.Observe(&installer);
installer.Start();
install_start_time_ = base::Time::Now();
OnStateUpdated();
}
BEGIN_METADATA(BorealisInstallerView, views::DialogDelegateView)
ADD_READONLY_PROPERTY_METADATA(std::u16string, PrimaryMessage)
ADD_READONLY_PROPERTY_METADATA(std::u16string, SecondaryMessage)
ADD_READONLY_PROPERTY_METADATA(int, CurrentDialogButtons)
END_METADATA