chromium/chrome/browser/ash/app_restore/arc_ghost_window_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 "chrome/browser/ash/app_restore/arc_ghost_window_view.h"

#include "ash/components/arc/arc_features.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/app_restore/arc_ghost_window_handler.h"
#include "chrome/browser/ash/app_restore/arc_ghost_window_shell_surface.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/arc/window_predictor/window_predictor_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_styles.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_throbber.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/throbber.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/layout_provider.h"

namespace ash::full_restore {

namespace {

constexpr char kGhostWindowTypeHistogram[] = "Arc.GhostWindowViewType";
constexpr int kAppIconSizeNewStyle = 64;
constexpr int kThrobberDiameterOriginalStyle = 24;
constexpr int kThrobberDiameterNewStyle = 24;
constexpr int kSpaceBetweenThrobberAndMessage = 12;
constexpr int kSpaceBetweenIconAndMessage = 48;

gfx::ImageSkia ResizeAndShadowedImage(const gfx::ImageSkia& image,
                                      gfx::Size size) {
  // TODO(sstan): Clear these definitions.
  // The shadow defined in ash/shelf/shelf_app_button.cc
  const std::vector<gfx::ShadowValue> kShadows = {
      gfx::ShadowValue(gfx::Vector2d(0, 2), 0, SkColorSetARGB(0x1A, 0, 0, 0)),
      gfx::ShadowValue(gfx::Vector2d(0, 3), 1, SkColorSetARGB(0x1A, 0, 0, 0)),
      gfx::ShadowValue(gfx::Vector2d(0, 0), 1, SkColorSetARGB(0x54, 0, 0, 0)),
  };
  return gfx::ImageSkiaOperations::CreateImageWithDropShadow(
      gfx::ImageSkiaOperations::CreateResizedImage(
          image, skia::ImageOperations::RESIZE_BEST, size),
      kShadows);
}

// Ghost window view type enumeration; Used for UMA counter.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class GhostWindowType {
  kIconSpinning = 0,
  kIconSpinningWithFixupText = 1,
  kMaxValue = kIconSpinningWithFixupText,
};

bool IsGhostWindowNewStyleEnabled() {
  return base::FeatureList::IsEnabled(arc::kGhostWindowNewStyle);
}

std::u16string GetGhostWindowAppLaunchString(const std::string& app_name) {
  return l10n_util::GetStringFUTF16(IDS_ARC_GHOST_WINDOW_APP_LAUNCHING_MESSAGE,
                                    base::UTF8ToUTF16(app_name));
}

std::u16string GetGhostWindowAppLaunchAodString() {
  return l10n_util::GetStringUTF16(
      IDS_ARC_GHOST_WINDOW_APP_LAUNCHING_AOD_MESSAGE);
}

class Throbber : public views::View {
  METADATA_HEADER(Throbber, views::View)

 public:
  explicit Throbber(uint32_t color) : color_(color) {
    start_time_ = base::TimeTicks::Now();
    timer_.Start(
        FROM_HERE, base::Milliseconds(30),
        base::BindRepeating(&Throbber::SchedulePaint, base::Unretained(this)));
    SchedulePaint();  // paint right away
    GetViewAccessibility().SetProperties(
        ax::mojom::Role::kProgressIndicator,
        l10n_util::GetStringUTF16(IDS_ARC_GHOST_WINDOW_APP_LAUNCHING_THROBBER));
  }
  Throbber(const Throbber&) = delete;
  Throbber operator=(const Throbber&) = delete;
  ~Throbber() override { timer_.Stop(); }

  void OnPaint(gfx::Canvas* canvas) override {
    base::TimeDelta elapsed_time = base::TimeTicks::Now() - start_time_;
    gfx::PaintThrobberSpinning(canvas, GetContentsBounds(), color_,
                               elapsed_time);
  }

 private:
  uint32_t color_;              // Throbber color.
  base::TimeTicks start_time_;  // Time when Start was called.
  base::RepeatingTimer timer_;  // Used to schedule Run calls.
};

BEGIN_METADATA(Throbber)
END_METADATA

}  // namespace

ArcGhostWindowView::ArcGhostWindowView(
    ArcGhostWindowShellSurface* shell_surface,
    const std::string& app_name)
    : app_name_(app_name),
      ghost_window_type_(arc::GhostWindowType::kAppLaunch),
      shell_surface_(shell_surface) {
  views::BoxLayout* layout =
      SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kVertical));
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  layout->set_between_child_spacing(
      views::LayoutProvider::Get()->GetDistanceMetric(
          views::DISTANCE_RELATED_CONTROL_HORIZONTAL));
}

ArcGhostWindowView::~ArcGhostWindowView() = default;

void ArcGhostWindowView::SetThemeColor(uint32_t theme_color) {
  theme_color_ = theme_color;
}

void ArcGhostWindowView::SetGhostWindowViewType(arc::GhostWindowType type) {
  ghost_window_type_ = type;
  RemoveAllChildViews();

  // DarkLightModeController maybe null in test env.
  if (type != arc::GhostWindowType::kFullRestore &&
      IsGhostWindowNewStyleEnabled() && DarkLightModeController::Get()) {
    // New style use ChromeOS system provided background color.
    auto color = cros_styles::ResolveColor(
        cros_styles::ColorName::kBgColor,
        DarkLightModeController::Get()->IsDarkModeEnabled());
    SetBackground(views::CreateSolidBackground(color));
    if (shell_surface_)
      shell_surface_->OnSetFrameColors(color, color);
  } else {
    // Use ARC app's theme color.
    SetBackground(views::CreateSolidBackground(theme_color_));
  }

  if (type == arc::GhostWindowType::kFullRestore ||
      !IsGhostWindowNewStyleEnabled()) {
    // If not enabled new style flag, all types will use original UI.
    AddChildView(views::Builder<views::ImageView>()
                     .SetImage(icon_raw_data_)
                     .SetAccessibleName(l10n_util::GetStringUTF16(
                         IDS_ARC_GHOST_WINDOW_APP_LAUNCHING_ICON))
                     .SetID(ContentID::ID_ICON_IMAGE)
                     .Build());

    auto* throbber = AddChildView(std::make_unique<Throbber>(
        color_utils::GetColorWithMaxContrast(theme_color_)));
    throbber->SetPreferredSize(gfx::Size(kThrobberDiameterOriginalStyle,
                                         kThrobberDiameterOriginalStyle));
    throbber->GetViewAccessibility().SetRole(ax::mojom::Role::kImage);
    throbber->SetID(ContentID::ID_THROBBER);
    // TODO(sstan): Set window title and accessible name from saved data.
  } else {
    if (type == arc::GhostWindowType::kFixup) {
      AddCommonChildrenViews();
      AddChildrenViewsForFixupType();
    } else if (type == arc::GhostWindowType::kAppLaunch) {
      AddCommonChildrenViews();
      AddChildrenViewsForAppLaunchType();
    } else {
      NOTREACHED_IN_MIGRATION();
    }
  }

  DeprecatedLayoutImmediately();
}

void ArcGhostWindowView::OnThemeChanged() {
  views::View::OnThemeChanged();
  // DarkLightModeController maybe null in test env.
  if (!IsGhostWindowNewStyleEnabled() ||
      ghost_window_type_ == arc::GhostWindowType::kFullRestore ||
      !DarkLightModeController::Get()) {
    return;
  }
  auto color = cros_styles::ResolveColor(
      cros_styles::ColorName::kBgColor,
      DarkLightModeController::Get()->IsDarkModeEnabled());
  SetBackground(views::CreateSolidBackground(color));
  if (shell_surface_)
    shell_surface_->OnSetFrameColors(color, color);
}

void ArcGhostWindowView::LoadIcon(const std::string& app_id) {
  Profile* profile = ProfileHelper::Get()->GetProfileByAccountId(
      user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId());
  DCHECK(profile);

  DCHECK(
      apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile));

  apps::AppServiceProxyFactory::GetForProfile(profile)->LoadIcon(
      app_id, apps::IconType::kStandard,
      SharedAppListConfig::instance().default_grid_icon_dimension(),
      /*allow_placeholder_icon=*/false,
      icon_loaded_cb_for_testing_.is_null()
          ? base::BindOnce(&ArcGhostWindowView::OnIconLoaded,
                           weak_ptr_factory_.GetWeakPtr())
          : std::move(icon_loaded_cb_for_testing_));
}

void ArcGhostWindowView::OnIconLoaded(apps::IconValuePtr icon_value) {
  if (!icon_value || icon_value->icon_type != apps::IconType::kStandard)
    return;

  icon_raw_data_ = icon_value->uncompressed;
  SetGhostWindowViewType(ghost_window_type_);
}

void ArcGhostWindowView::AddCommonChildrenViews() {
  static_cast<views::BoxLayout*>(GetLayoutManager())
      ->set_between_child_spacing(kSpaceBetweenIconAndMessage);
  AddChildView(views::Builder<views::ImageView>()
                   .SetImage(ResizeAndShadowedImage(
                       icon_raw_data_,
                       gfx::Size(kAppIconSizeNewStyle, kAppIconSizeNewStyle)))
                   .SetAccessibleName(l10n_util::GetStringUTF16(
                       IDS_ARC_GHOST_WINDOW_APP_LAUNCHING_ICON))
                   .SetID(ContentID::ID_ICON_IMAGE)
                   .Build());
}

void ArcGhostWindowView::AddChildrenViewsForFixupType() {
  AddChildView(
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::BoxLayout::Orientation::kVertical)
          .SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kCenter)
          .SetBetweenChildSpacing(kSpaceBetweenThrobberAndMessage)
          .AddChildren(
              views::Builder<views::Throbber>()
                  .SetID(ContentID::ID_THROBBER)
                  .SetPreferredSize(gfx::Size(kThrobberDiameterNewStyle,
                                              kThrobberDiameterNewStyle)),
              views::Builder<views::Label>()
                  .SetText(l10n_util::GetStringUTF16(
                      IDS_ARC_GHOST_WINDOW_APP_FIXUP_MESSAGE))
                  .SetTextStyle(views::style::STYLE_SECONDARY)
                  .SetMultiLine(true)
                  .SetID(ContentID::ID_MESSAGE_LABEL))
          .Build());

  static_cast<views::Throbber*>(GetViewByID(ContentID::ID_THROBBER))->Start();
  base::UmaHistogramEnumeration(kGhostWindowTypeHistogram,
                                GhostWindowType::kIconSpinningWithFixupText);
}

void ArcGhostWindowView::AddChildrenViewsForAppLaunchType() {
  auto app_launch_message = arc::ArcSessionManager::Get()->IsActivationDelayed()
                                ? GetGhostWindowAppLaunchAodString()
                                : GetGhostWindowAppLaunchString(app_name_);
  AddChildView(
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::BoxLayout::Orientation::kHorizontal)
          .SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kCenter)
          .SetBetweenChildSpacing(kSpaceBetweenThrobberAndMessage)
          .AddChildren(
              views::Builder<views::Throbber>()
                  .SetID(ContentID::ID_THROBBER)
                  .SetPreferredSize(gfx::Size(kThrobberDiameterNewStyle,
                                              kThrobberDiameterNewStyle)),
              views::Builder<views::Label>()
                  .SetText(app_launch_message)
                  .SetTextContext(views::style::CONTEXT_LABEL)
                  .SetTextStyle(views::style::STYLE_SECONDARY)
                  .SetMultiLine(true)
                  .SetID(ContentID::ID_MESSAGE_LABEL))
          .Build());

  static_cast<views::Throbber*>(GetViewByID(ContentID::ID_THROBBER))->Start();
  base::UmaHistogramEnumeration(kGhostWindowTypeHistogram,
                                GhostWindowType::kIconSpinning);
}
BEGIN_METADATA(ArcGhostWindowView)
END_METADATA

}  // namespace ash::full_restore