chromium/ash/components/arc/compat_mode/arc_splash_screen_dialog_view.cc

// Copyright 2021 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/components/arc/compat_mode/arc_splash_screen_dialog_view.h"

#include <memory>

#include "ash/components/arc/compat_mode/overlay_dialog.h"
#include "ash/components/arc/compat_mode/style/arc_color_provider.h"
#include "ash/components/arc/vector_icons/vector_icons.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "base/auto_reset.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/scoped_multi_source_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/frame/caption_buttons/frame_center_button.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

namespace arc {

namespace {

// Draws the blue-ish highlight border to the parent view according to the
// highlight path.
class HighlightBorder : public views::View {
  METADATA_HEADER(HighlightBorder, views::View)

 public:
  HighlightBorder() = default;
  HighlightBorder(const HighlightBorder&) = delete;
  HighlightBorder& operator=(const HighlightBorder&) = delete;
  ~HighlightBorder() override = default;

  // views::View:
  void OnThemeChanged() override {
    views::View::OnThemeChanged();
    InvalidateLayout();
    SchedulePaint();
  }

  void Layout(PassKey) override {
    auto bounds = parent()->GetLocalBounds();
    bounds.Inset(gfx::Insets(views::FocusRing::kDefaultHaloInset));
    SetBoundsRect(bounds);
  }

  void OnPaint(gfx::Canvas* canvas) override {
    views::View::OnPaint(canvas);

    const auto rrect =
        views::HighlightPathGenerator::GetRoundRectForView(parent());
    if (!rrect)
      return;
    auto rect = (*rrect).rect();
    View::ConvertRectToTarget(parent(), this, &rect);
    cc::PaintFlags flags;
    flags.setAntiAlias(true);
    flags.setColor(
        GetColorProvider()->GetColor(cros_tokens::kCrosSysFocusRing));
    flags.setStyle(cc::PaintFlags::kStroke_Style);
    flags.setStrokeWidth(views::FocusRing::kDefaultHaloThickness);
    canvas->DrawRoundRect(rect, (*rrect).GetSimpleRadius(), flags);
  }
};

BEGIN_METADATA(HighlightBorder)
END_METADATA

}  // namespace

class ArcSplashScreenDialogView::ArcSplashScreenWindowObserver
    : public aura::WindowObserver {
 public:
  ArcSplashScreenWindowObserver(aura::Window* window,
                                base::RepeatingClosure on_close_callback)
      : on_close_callback_(on_close_callback) {
    window_observation_.Observe(window);
  }

  ArcSplashScreenWindowObserver(const ArcSplashScreenWindowObserver&) = delete;
  ArcSplashScreenWindowObserver& operator=(
      const ArcSplashScreenWindowObserver&) = delete;
  ~ArcSplashScreenWindowObserver() override = default;

 private:
  // aura::WindowObserver:
  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    if (key != aura::client::kShowStateKey)
      return;

    ui::WindowShowState state =
        window->GetProperty(aura::client::kShowStateKey);
    if (state == ui::SHOW_STATE_FULLSCREEN ||
        state == ui::SHOW_STATE_MAXIMIZED) {
      // Run the callback when window is fullscreen or maximized.
      on_close_callback_.Run();
    }
  }

  void OnWindowDestroying(aura::Window* window) override {
    window_observation_.Reset();
  }

  base::RepeatingClosure on_close_callback_;
  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};
};

ArcSplashScreenDialogView::ArcSplashScreenDialogView(
    base::OnceClosure close_callback,
    aura::Window* parent,
    views::View* anchor,
    bool is_for_unresizable)
    : anchor_(anchor),
      close_callback_(std::move(close_callback)),
      background_color_id_(cros_tokens::kCrosSysDialogContainer) {
  // Setup delegate.
  SetArrow(views::BubbleBorder::Arrow::BOTTOM_CENTER);
  SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
  set_parent_window(parent);
  set_title_margins(gfx::Insets());
  set_margins(gfx::Insets());
  SetAnchorView(anchor_);
  SetTitle(l10n_util::GetStringUTF16(IDS_ARC_COMPAT_MODE_SPLASH_SCREEN_TITLE));
  SetShowTitle(false);
  SetAccessibleWindowRole(ax::mojom::Role::kDialog);
  // For handling the case when Esc key is pressed.
  SetCancelCallback(
      base::BindOnce(&ArcSplashScreenDialogView::OnCloseButtonClicked,
                     weak_ptr_factory_.GetWeakPtr()));
  set_adjust_if_offscreen(false);
  set_close_on_deactivate(false);

  // Setup views.
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kVertical)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetInteriorMargin(gfx::Insets::TLBR(32, 32, 32, 28))
      .SetDefault(
          views::kFlexBehaviorKey,
          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                                   views::MaximumFlexSizeRule::kPreferred,
                                   /*adjust_height_for_width=*/true));

  auto image =
      ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
          IDR_ARC_COMPAT_MODE_SPLASH_SCREEN_IMAGE);
  AddChildView(
      views::Builder<views::ImageView>()  // Logo
          .SetImage(image)
          .SetProperty(views::kMarginsKey, gfx::Insets::TLBR(0, 0, 16, 0))
          .Build());

  const raw_ptr<views::Label> title_label =
      AddChildView(views::Builder<views::Label>()  // Header
                       .SetText(l10n_util::GetStringUTF16(
                           IDS_ARC_COMPAT_MODE_SPLASH_SCREEN_TITLE))
                       .SetTextContext(views::style::CONTEXT_DIALOG_TITLE)
                       .SetHorizontalAlignment(gfx::ALIGN_CENTER)
                       .SetAllowCharacterBreak(true)
                       .SetMultiLine(true)
                       .SetProperty(views::kMarginsKey, gfx::Insets::VH(16, 0))
                       .Build());
  ash::TypographyProvider::Get()->StyleLabel(
      ash::TypographyToken::kCrosDisplay7, *title_label);
  title_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);

  const raw_ptr<views::Label> body_label = AddChildView(
      views::Builder<views::Label>()  // Body
          .SetText(is_for_unresizable
                       ? l10n_util::GetStringUTF16(
                             IDS_ARC_COMPAT_MODE_SPLASH_SCREEN_BODY_UNRESIZABLE)
                       : l10n_util::GetStringFUTF16(
                             IDS_ARC_COMPAT_MODE_SPLASH_SCREEN_BODY,
                             parent->GetTitle()))
          .SetTextStyle(views::style::STYLE_SECONDARY)
          .SetTextContext(views::style::TextContext::CONTEXT_DIALOG_BODY_TEXT)
          .SetHorizontalAlignment(gfx::ALIGN_CENTER)
          .SetMultiLine(true)
          .Build());
  ash::TypographyProvider::Get()->StyleLabel(ash::TypographyToken::kCrosBody1,
                                             *body_label);
  body_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurfaceVariant);

  AddChildView(
      views::Builder<ash::PillButton>()  // Close button
          .CopyAddressTo(&close_button_)
          .SetCallback(base::BindRepeating(
              &ArcSplashScreenDialogView::OnCloseButtonClicked,
              base::Unretained(this)))
          .SetText(l10n_util::GetStringUTF16(
              IDS_ARC_COMPAT_MODE_SPLASH_SCREEN_CLOSE))
          .SetPillButtonType(ash::PillButton::kPrimaryLargeWithoutIcon)
          .SetIsDefault(true)
          .SetProperty(views::kMarginsKey, gfx::Insets::TLBR(32, 0, 0, 0))
          .Build());

  // Setup highlight border.
  highlight_border_ =
      anchor_->AddChildView(std::make_unique<HighlightBorder>());

  // Observe anchor and its highlight to be notified when it's destroyed.
  anchor_highlight_observations_.AddObservation(anchor_.get());
  anchor_highlight_observations_.AddObservation(highlight_border_.get());

  // Add window observer.
  window_observer_ = std::make_unique<ArcSplashScreenWindowObserver>(
      parent,
      base::BindRepeating(&ArcSplashScreenDialogView::OnCloseButtonClicked,
                          base::Unretained(this)));

  activation_observation_.Observe(
      wm::GetActivationClient(parent_window()->GetRootWindow()));
}

ArcSplashScreenDialogView::~ArcSplashScreenDialogView() = default;

gfx::Size ArcSplashScreenDialogView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  auto width = views::LayoutProvider::Get()->GetDistanceMetric(
      views::DistanceMetric::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
  const auto* widget = GetWidget();
  if (widget && widget->parent()) {
    const int kHorizontalMarginDp = 36;
    width = std::min(widget->parent()->GetWindowBoundsInScreen().width() -
                         kHorizontalMarginDp * 2,
                     width);
  }
  return gfx::Size(width,
                   GetLayoutManager()->GetPreferredHeightForWidth(this, width));
}

gfx::Rect ArcSplashScreenDialogView::GetBubbleBounds() {
  gfx::Rect bubble_bounds = BubbleDialogDelegate::GetBubbleBounds();
  constexpr int kMarginTopDp = 8;
  bubble_bounds.Offset(0, kMarginTopDp);
  return bubble_bounds;
}

void ArcSplashScreenDialogView::AddedToWidget() {
  const int kCornerRadius = 20;
  auto* const frame = GetBubbleFrameView();
  if (frame)
    frame->SetCornerRadius(kCornerRadius);
}

void ArcSplashScreenDialogView::OnThemeChanged() {
  views::BubbleDialogDelegateView::OnThemeChanged();
  set_color(GetColorProvider()->GetColor(background_color_id_));
}

void ArcSplashScreenDialogView::OnViewIsDeleting(View* observed_view) {
  if (observed_view == anchor_)
    anchor_ = nullptr;
  else if (observed_view == highlight_border_)
    highlight_border_ = nullptr;
  else
    NOTREACHED();

  anchor_highlight_observations_.RemoveObservation(observed_view);
}

void ArcSplashScreenDialogView::OnWindowActivated(ActivationReason reason,
                                                  aura::Window* gained_active,
                                                  aura::Window* lost_active) {
  if (gained_active != parent_window())
    return;

  // Safe-guard for the activation forwarding loop.
  if (forwarding_activation_)
    return;

  forwarding_activation_ = true;
  // Forward the activation to the dialog if available.
  // To avoid nested-activation, here we post the task to the queue.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(
                     [](base::WeakPtr<ArcSplashScreenDialogView> view) {
                       if (!view)
                         return;

                       base::AutoReset<bool> forwarding_activation_update(
                           &view->forwarding_activation_, false);
                       auto* const widget = view->GetWidget();
                       if (!widget)
                         return;
                       if (widget->IsClosed())
                         return;
                       widget->Activate();
                     },
                     weak_ptr_factory_.GetWeakPtr()));
}

void ArcSplashScreenDialogView::OnCloseButtonClicked() {
  if (!close_callback_)
    return;

  if (anchor_ && highlight_border_)
    anchor_->RemoveChildViewT(highlight_border_.get());

  std::move(close_callback_).Run();

  auto* const widget = GetWidget();
  if (widget)
    widget->CloseWithReason(views::Widget::ClosedReason::kCloseButtonClicked);
}

void ArcSplashScreenDialogView::Show(aura::Window* parent,
                                     bool is_for_unresizable) {
  auto* const frame_view = ash::NonClientFrameViewAsh::Get(parent);
  DCHECK(frame_view);
  auto* const anchor_view =
      frame_view->GetHeaderView()->GetFrameHeader()->GetCenterButton();

  if (!anchor_view) {
    LOG(ERROR) << "Failed to show the compat mode splash screen because the "
                  "center button is missing.";
    return;
  }

  auto dialog_view = std::make_unique<ArcSplashScreenDialogView>(
      base::BindOnce(&OverlayDialog::CloseIfAny, base::Unretained(parent)),
      parent, anchor_view, is_for_unresizable);

  OverlayDialog::Show(
      parent,
      base::BindOnce(&ArcSplashScreenDialogView::OnCloseButtonClicked,
                     dialog_view->weak_ptr_factory_.GetWeakPtr()),
      /*dialog_view=*/nullptr);

  // TODO(b/206336651): Investigate the cases when the following check fails.
  if (!anchor_view->GetWidget() ||
      !anchor_view->GetWidget()->GetNativeWindow()) {
    LOG(WARNING) << "Skipped to show the compat mode splash screen because the "
                    "anchored widget/window has already been destroyed.";
    return;
  }
  views::BubbleDialogDelegateView::CreateBubble(std::move(dialog_view))->Show();
}

BEGIN_METADATA(ArcSplashScreenDialogView)
END_METADATA

}  // namespace arc