chromium/chrome/browser/ui/views/borealis/borealis_launch_error_dialog.cc

// 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/borealis/borealis_launch_error_dialog.h"

#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "chrome/browser/ash/borealis/borealis_app_launcher.h"
#include "chrome/browser/ash/borealis/borealis_features.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/feedback/show_feedback_page.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/views/borealis/borealis_disallowed_dialog.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"

namespace views::borealis {

using ::borealis::BorealisStartupResult;

namespace {

// Views uses tricks like this to ensure singleton-ness of dialogs.
static Widget* g_instance_ = nullptr;

class BorealisLaunchErrorDialog : public DialogDelegate {
 public:
  explicit BorealisLaunchErrorDialog(Profile* profile,
                                     BorealisStartupResult error) {
    DCHECK(!g_instance_);

    SetTitle(IDS_BOREALIS_APP_NAME);
    set_internal_name("BorealisLaunchErrorDialog");

    failure_ = IdentifyFailure(error);

    std::u16string ok_string;
    switch (failure_) {
      case FailureType::FAILURE_NEED_RESTART:
        ok_string =
            l10n_util::GetStringUTF16(IDS_BOREALIS_LAUNCH_ERROR_BUTTON_RESTART);
        break;
      case FailureType::FAILURE_NEED_SPACE:
        ok_string = l10n_util::GetStringUTF16(
            IDS_BOREALIS_INSTALLER_ERROR_BUTTON_STORAGE);
        break;
      case FailureType::FAILURE_RETRY:
        ok_string = l10n_util::GetStringUTF16(
            IDS_BOREALIS_INSTALLER_ERROR_BUTTON_RETRY);
    }

    SetButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
               static_cast<int>(ui::mojom::DialogButton::kCancel));
    SetButtonLabel(ui::mojom::DialogButton::kOk, ok_string);
    SetButtonLabel(ui::mojom::DialogButton::kCancel,
                   l10n_util::GetStringUTF16(IDS_APP_CANCEL));

    SetCancelCallback(base::BindOnce(&BorealisLaunchErrorDialog::OnCancelled,
                                     base::Unretained(this), profile));

    SetAcceptCallback(base::BindOnce(&BorealisLaunchErrorDialog::OnAccepted,
                                     base::Unretained(this), profile));

    InitializeView();
    SetModalType(ui::mojom::ModalType::kNone);
    SetOwnedByWidget(true);
    SetShowCloseButton(false);
    set_fixed_width(ChromeLayoutProvider::Get()->GetDistanceMetric(
        views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH));
  }

  ~BorealisLaunchErrorDialog() override {
    DCHECK(g_instance_);
    g_instance_ = nullptr;
  }

  bool ShouldShowWindowTitle() const override { return false; }

 private:
  // Used to identify the failure type.
  enum FailureType {
    FAILURE_NEED_RESTART,
    FAILURE_NEED_SPACE,
    FAILURE_RETRY,
  };

  FailureType IdentifyFailure(BorealisStartupResult error) {
    switch (error) {
      case BorealisStartupResult::kDlcNeedRebootError:
      case BorealisStartupResult::kDlcNeedUpdateError:
        return FAILURE_NEED_RESTART;
      case BorealisStartupResult::kDlcNeedSpaceError:
        return FAILURE_NEED_SPACE;
      case BorealisStartupResult::kAwaitBorealisStartupFailed:
      case BorealisStartupResult::kDiskImageFailed:
      case BorealisStartupResult::kDlcCancelled:
      case BorealisStartupResult::kDlcOffline:
      case BorealisStartupResult::kDlcBusyError:
      case BorealisStartupResult::kDlcInternalError:
      case BorealisStartupResult::kStartVmEmptyResponse:
      case BorealisStartupResult::kStartVmFailed:
      case BorealisStartupResult::kEmptyDiskResponse:
      default:
        return FAILURE_RETRY;
    }
  }

  void InitializeView() {
    auto view = std::make_unique<views::View>();

    views::LayoutProvider* provider = views::LayoutProvider::Get();
    view->SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kVertical,
        provider->GetInsetsMetric(views::InsetsMetric::INSETS_DIALOG),
        provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));

    views::Label* title_label = new views::Label(
        l10n_util::GetStringUTF16(IDS_BOREALIS_LAUNCH_ERROR_TITLE),
        CONTEXT_IPH_BUBBLE_TITLE, views::style::STYLE_EMPHASIZED);
    title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    title_label->SetMultiLine(true);
    view->AddChildView(title_label);

    std::u16string body_string;
    switch (failure_) {
      case FailureType::FAILURE_NEED_RESTART:
        body_string = l10n_util::GetStringUTF16(
            IDS_BOREALIS_LAUNCH_ERROR_NEED_RESTART_BODY);
        break;
      case FailureType::FAILURE_NEED_SPACE:
        body_string = l10n_util::GetStringUTF16(
            IDS_BOREALIS_LAUNCH_ERROR_NEED_SPACE_BODY);
        break;
      case FailureType::FAILURE_RETRY:
        body_string = l10n_util::GetStringUTF16(IDS_BOREALIS_LAUNCH_ERROR_BODY);
    }
    views::Label* message_label = new views::Label(body_string);
    message_label->SetMultiLine(true);
    message_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    view->AddChildView(message_label);

    if (failure_ == FailureType::FAILURE_RETRY) {
      auto checkbox = std::make_unique<views::Checkbox>(
          l10n_util::GetStringUTF16(IDS_BOREALIS_LAUNCH_ERROR_SEND_FEEDBACK));
      feedback_checkbox_ = view->AddChildView(std::move(checkbox));
    }

    SetContentsView(std::move(view));
  }

  void OnCancelled(Profile* profile) {
    if (feedback_checkbox_ && feedback_checkbox_->GetChecked()) {
      ShowFeedbackPage(profile);
    }
  }

  void OnAccepted(Profile* profile) {
    if (feedback_checkbox_ && feedback_checkbox_->GetChecked()) {
      ShowFeedbackPage(profile);
    }

    if (failure_ == FailureType::FAILURE_NEED_RESTART) {
      chromeos::PowerManagerClient::Get()->RequestRestart(
          power_manager::REQUEST_RESTART_FOR_USER,
          "borealis failure need restart");
    } else if (failure_ == FailureType::FAILURE_NEED_SPACE) {
      chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
          profile, chromeos::settings::mojom::kStorageSubpagePath);
    } else if (failure_ == FailureType::FAILURE_RETRY) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(
                         [](Profile* profile) {
                           // Technically "retry" should re-do whatever the user
                           // originally tried. For simplicity we just retry the
                           // client app.
                           ::borealis::BorealisService::GetForProfile(profile)
                               ->AppLauncher()
                               .Launch(::borealis::kClientAppId,
                                       ::borealis::BorealisLaunchSource::
                                           kErrorDialogRetryButton,
                                       base::DoNothing());
                         },
                         profile));
    }
  }

  void ShowFeedbackPage(Profile* profile) {
    chrome::ShowFeedbackPage(
        /*page_url=*/GURL(), profile, feedback::kFeedbackSourceBorealis,
        /*description_template=*/std::string(),
        /*description_placeholder_text=*/
        l10n_util::GetStringUTF8(IDS_BOREALIS_FEEDBACK_PLACEHOLDER),
        /*category_tag=*/"borealis",
        /*extra_diagnostics=*/std::string());
  }

  FailureType failure_;
  raw_ptr<views::Checkbox> feedback_checkbox_ = nullptr;
};
}  // namespace

void ShowBorealisLaunchErrorView(Profile* profile,
                                 BorealisStartupResult error) {
  if (error == BorealisStartupResult::kDisallowed) {
    ::borealis::BorealisService::GetForProfile(profile)->Features().IsAllowed(
        base::BindOnce(&ShowLauncherDisallowedDialog));
    return;
  }

  // TODO(b/248938308): Closing and reopening the dialog this way is not
  // desirable. When we move to webui we should just re-show the current dialog.
  if (g_instance_) {
    g_instance_->CloseNow();
  }

  auto delegate = std::make_unique<BorealisLaunchErrorDialog>(profile, error);
  g_instance_ = views::DialogDelegate::CreateDialogWidget(std::move(delegate),
                                                          nullptr, nullptr);
  g_instance_->GetNativeWindow()->SetProperty(
      ash::kShelfIDKey, ash::ShelfID(::borealis::kClientAppId).Serialize());
  g_instance_->Show();
}

}  // namespace views::borealis