chromium/ash/system/phonehub/app_stream_connection_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 "ash/system/phonehub/app_stream_connection_error_dialog.h"

#include <memory>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ash/components/phonehub/url_constants.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/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/text_constants.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_shadow.h"
#include "ui/views/window/dialog_delegate.h"
#include "ui/views/window/non_client_view.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

namespace {

// Offset to place dialog in the vertical center of bubble due to
// PhoneStatusView.
constexpr int kDialogVerticalOffset = 25;

constexpr int kDialogWidth = 330;

constexpr gfx::Insets kDialogContentInsets = gfx::Insets::VH(20, 24);
constexpr float kDialogRoundedCornerRadius = 16.0f;
constexpr int kDialogShadowElevation = 3;

constexpr int kIconSize = 25;

constexpr int kMarginBetweenIconAndTitle = 15;
constexpr int kMarginBetweenTitleAndBody = 15;
constexpr int kMarginBetweenBodyAndButtons = 20;
constexpr int kMarginBetweenButtons = 8;

// The real error dialog with content.
class ConnectionErrorDialogDelegateView : public views::WidgetDelegateView {
  METADATA_HEADER(ConnectionErrorDialogDelegateView, views::WidgetDelegateView)
 public:
  ConnectionErrorDialogDelegateView(
      StartTetheringCallback start_tethering_callback,
      bool is_on_different_network,
      bool is_phone_on_cellular)
      : start_tethering_callback_(std::move(start_tethering_callback)) {
    SetModalType(ui::mojom::ModalType::kWindow);

    SetPaintToLayer();
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
    layer()->SetRoundedCornerRadius(
        gfx::RoundedCornersF(kDialogRoundedCornerRadius));

    SetBackground(views::CreateThemedRoundedRectBackground(
        static_cast<ui::ColorId>(cros_tokens::kCrosSysBaseElevated),
        kDialogRoundedCornerRadius));
    SetBorder(std::make_unique<views::HighlightBorder>(
        kDialogRoundedCornerRadius,
        views::HighlightBorder::Type::kHighlightBorder1));

    view_shadow_ =
        std::make_unique<views::ViewShadow>(this, kDialogShadowElevation);
    view_shadow_->SetRoundedCornerRadius(kDialogRoundedCornerRadius);

    SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kVertical, kDialogContentInsets));

    // Add info icon.
    auto* icon_row = AddChildView(std::make_unique<views::View>());
    icon_row
        ->SetLayoutManager(std::make_unique<views::BoxLayout>(
            views::BoxLayout::Orientation::kHorizontal, gfx::Insets()))
        ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
    icon_ = icon_row->AddChildView(
        std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
            kPhoneHubEcheErrorStatusIcon,
            AshColorProvider::Get()->GetContentLayerColor(
                AshColorProvider::ContentLayerType::kIconColorWarning),
            kIconSize)));

    // Add dialog title.
    title_ =
        AddChildView(std::make_unique<views::Label>(l10n_util::GetStringUTF16(
            IDS_ASH_ECHE_APP_STREMING_ERROR_DIALOG_TITLE)));
    title_->SetProperty(views::kMarginsKey,
                        gfx::Insets::TLBR(kMarginBetweenIconAndTitle, 0, 0, 0));
    title_->SetTextContext(views::style::CONTEXT_DIALOG_TITLE);
    title_->SetTextStyle(views::style::STYLE_EMPHASIZED);
    title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    title_->SetAutoColorReadabilityEnabled(false);

    TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosTitle1,
                                          *title_);
    title_->SetPaintToLayer();
    title_->layer()->SetFillsBoundsOpaquely(false);

    // Add dialog body.

    body_ = AddChildView(std::make_unique<views::StyledLabel>());

    std::u16string body_text;
    const std::u16string learn_more_link =
        l10n_util::GetStringUTF16(IDS_ASH_LEARN_MORE);
    // To record where "Learn more" text begin in the dialog body.
    size_t offset;
    if (is_phone_on_cellular) {
      body_text = l10n_util::GetStringFUTF16(
          IDS_ASH_ECHE_APP_STREAMING_ERROR_DIALOG_PHONE_ON_CELLULAR_TEXT,
          learn_more_link, &offset);
    } else if (is_on_different_network) {
      body_text = l10n_util::GetStringFUTF16(
          IDS_ASH_ECHE_APP_STREAMING_ERROR_DIALOG_DIFFERENT_NETWORK_TEXT,
          learn_more_link, &offset);
    } else {
      body_text = l10n_util::GetStringFUTF16(
          IDS_ASH_ECHE_APP_STREAMING_ERROR_DIALOG_UNSUPPORTED_NETWORK_TEXT,
          learn_more_link, &offset);
    }
    body_->SetText(body_text);

    views::StyledLabel::RangeStyleInfo style;
    style.override_color = AshColorProvider::Get()->GetContentLayerColor(
        AshColorProvider::ContentLayerType::kTextColorPrimary);
    body_->AddStyleRange(gfx::Range(0, offset), style);

    views::StyledLabel::RangeStyleInfo link_style =
        views::StyledLabel::RangeStyleInfo::CreateForLink(base::BindRepeating(
            &ConnectionErrorDialogDelegateView::LearnMoreLinkPressed,
            base::Unretained(this),
            base::BindRepeating(
                &NewWindowDelegate::OpenUrl,
                base::Unretained(NewWindowDelegate::GetPrimary()),
                GURL(phonehub::kPhoneHubLearnMoreLink),
                NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                NewWindowDelegate::Disposition::kNewForegroundTab)));
    const SkColor link_color = AshColorProvider::Get()->GetContentLayerColor(
        AshColorProvider::ContentLayerType::kButtonLabelColorBlue);
    link_style.override_color = link_color;
    body_->AddStyleRange(gfx::Range(offset, offset + learn_more_link.length()),
                         link_style);

    body_->SetProperty(views::kMarginsKey,
                       gfx::Insets::TLBR(kMarginBetweenTitleAndBody, 0,
                                         kMarginBetweenBodyAndButtons, 0));
    body_->SetTextContext(views::style::CONTEXT_DIALOG_BODY_TEXT);
    body_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    body_->SetAutoColorReadabilityEnabled(false);

    body_->SetPaintToLayer();
    body_->layer()->SetFillsBoundsOpaquely(false);

    // TODO(b/254874005): Migrate the |body_| font to Google Sans. Use the same
    // TypographyProvider StyleLabel() but use ash::Typography::kCrosBody.

    // Add button row.
    auto* button_row = AddChildView(std::make_unique<views::View>());
    button_row
        ->SetLayoutManager(std::make_unique<views::BoxLayout>(
            views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
            kMarginBetweenButtons))
        ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kEnd);

    if (!is_on_different_network && !is_phone_on_cellular) {
      cancel_button_ =
          button_row->AddChildView(std::make_unique<ash::PillButton>(
              views::Button::PressedCallback(base::BindRepeating(
                  &ConnectionErrorDialogDelegateView::OnCancelClicked,
                  base::Unretained(this))),
              l10n_util::GetStringUTF16(
                  IDS_ASH_ECHE_APP_STREAMING_ERROR_DIALOG_DISMISS_TEXT),
              PillButton::Type::kDefaultWithoutIcon, nullptr));
      accept_button_ =
          button_row->AddChildView(std::make_unique<ash::PillButton>(
              views::Button::PressedCallback(base::BindRepeating(
                  &ConnectionErrorDialogDelegateView::OnStartTetheringClicked,
                  base::Unretained(this))),
              l10n_util::GetStringUTF16(
                  IDS_ASH_ECHE_APP_STREMING_ERROR_DIALOG_TURN_ON_HOTSPOT),
              PillButton::Type::kPrimaryWithoutIcon, nullptr));
    } else {
      cancel_button_ =
          button_row->AddChildView(std::make_unique<ash::PillButton>(
              views::Button::PressedCallback(base::BindRepeating(
                  &ConnectionErrorDialogDelegateView::OnCancelClicked,
                  base::Unretained(this))),
              l10n_util::GetStringUTF16(
                  IDS_ASH_ECHE_APP_STREAMING_ERROR_DIALOG_DISMISS_TEXT),
              PillButton::Type::kPrimaryWithoutIcon, nullptr));
    }
  }

  ConnectionErrorDialogDelegateView(const ConnectionErrorDialogDelegateView&) =
      delete;
  ConnectionErrorDialogDelegateView& operator=(
      const ConnectionErrorDialogDelegateView&) = delete;

  ~ConnectionErrorDialogDelegateView() override = default;

  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    return gfx::Size(
        kDialogWidth,
        GetLayoutManager()->GetPreferredHeightForWidth(this, kDialogWidth));
  }

  void OnStartTetheringClicked(const ui::Event& event) {
    if (start_tethering_callback_) {
      std::move(start_tethering_callback_).Run(event);
    }

    GetWidget()->CloseWithReason(
        views::Widget::ClosedReason::kAcceptButtonClicked);
  }

  void OnCancelClicked() {
    GetWidget()->CloseWithReason(
        views::Widget::ClosedReason::kCancelButtonClicked);
  }

  void LearnMoreLinkPressed(base::RepeatingClosure callback) {
    std::move(callback).Run();
  }

 private:
  StartTetheringCallback start_tethering_callback_;
  std::unique_ptr<views::ViewShadow> view_shadow_;

  raw_ptr<views::ImageView> icon_ = nullptr;
  raw_ptr<views::Label> title_ = nullptr;
  raw_ptr<views::StyledLabel> body_ = nullptr;
  raw_ptr<views::Button> cancel_button_ = nullptr;
  raw_ptr<views::Button> accept_button_ = nullptr;
};

BEGIN_METADATA(ConnectionErrorDialogDelegateView)
END_METADATA

}  // namespace

AppStreamConnectionErrorDialog::AppStreamConnectionErrorDialog(
    views::View* host_view,
    base::OnceClosure on_close_callback,
    StartTetheringCallback button_callback,
    bool is_different_network,
    bool is_phone_one_cellular)
    : host_view_(host_view), on_close_callback_(std::move(on_close_callback)) {
  auto dialog = std::make_unique<ConnectionErrorDialogDelegateView>(
      std::move(button_callback), is_different_network, is_phone_one_cellular);
  views::Widget* const parent = host_view_->GetWidget();

  widget_ = new views::Widget();
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.layer_type = ui::LAYER_NOT_DRAWN;
  params.parent = parent->GetNativeWindow();
  params.delegate = dialog.release();

  widget_->Init(std::move(params));

  // The |dialog| ownership is passed to the window hierarchy.
  widget_observations_.AddObservation(widget_.get());
  widget_observations_.AddObservation(parent);

  view_observations_.AddObservation(host_view_.get());
  view_observations_.AddObservation(widget_->GetContentsView());
}

AppStreamConnectionErrorDialog::~AppStreamConnectionErrorDialog() {
  view_observations_.RemoveAllObservations();
  widget_observations_.RemoveAllObservations();
  if (widget_) {
    widget_->Close();
    widget_ = nullptr;
  }
}

void AppStreamConnectionErrorDialog::UpdateBounds() {
  if (!widget_) {
    return;
  }

  gfx::Point anchor_point_in_screen(host_view_->width() / 2,
                                    host_view_->height() / 2);
  views::View::ConvertPointToScreen(host_view_, &anchor_point_in_screen);

  gfx::Size dialog_size = widget_->GetContentsView()->GetPreferredSize();
  widget_->SetBounds(gfx::Rect(
      gfx::Point(anchor_point_in_screen.x() - dialog_size.width() / 2,
                 anchor_point_in_screen.y() - dialog_size.height() / 2 -
                     kDialogVerticalOffset),
      dialog_size));
}

void AppStreamConnectionErrorDialog::OnWidgetDestroying(views::Widget* widget) {
  if (on_close_callback_) {
    std::move(on_close_callback_).Run();
  }
}

void AppStreamConnectionErrorDialog::OnWidgetBoundsChanged(
    views::Widget* widget,
    const gfx::Rect& new_bounds) {
  if (widget == host_view_->GetWidget()) {
    UpdateBounds();
  }
}

void AppStreamConnectionErrorDialog::OnViewBoundsChanged(
    views::View* observed_view) {
  UpdateBounds();
}

void AppStreamConnectionErrorDialog::OnViewPreferredSizeChanged(
    views::View* observed_view) {
  UpdateBounds();
}

}  // namespace ash