chromium/ash/capture_mode/capture_mode_education_controller.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/capture_mode/capture_mode_education_controller.h"

#include "ash/capture_mode/capture_mode_util.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/keyboard_shortcut_view.h"
#include "ash/style/system_dialog_delegate_view.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/unified/unified_system_tray.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// A nudge/tutorial will not be shown if it already been shown 3 times, or if 24
// hours have not yet passed since it was last shown.
constexpr int kNudgeMaxShownCount = 3;
constexpr base::TimeDelta kNudgeTimeBetweenShown = base::Hours(24);

constexpr char kCaptureModeNudgeId[] = "kCaptureModeNudge";

// Tutorial styling values.
constexpr int kRowSpacing = 30;
constexpr int kTitleShortcutSpacing = 8;
constexpr int kImageButtonSpacing = 20;
constexpr int kKeyboardImageWidth = 448;

// Clock that can be overridden for testing.
base::Clock* g_clock_override = nullptr;

PrefService* GetPrefService() {
  return Shell::Get()->session_controller()->GetActivePrefService();
}

base::Time GetTime() {
  return g_clock_override ? g_clock_override->Now() : base::Time::Now();
}

// Creates nudge data common to Arms 1 and 2.
AnchoredNudgeData CreateBaseNudgeData(NudgeCatalogName catalog_name) {
  AnchoredNudgeData nudge_data(
      kCaptureModeNudgeId, catalog_name,
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_LABEL));

  nudge_data.image_model =
      ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
          IDR_SCREEN_CAPTURE_EDUCATION_NUDGE_IMAGE);
  nudge_data.fill_image_size = true;
  nudge_data.keyboard_codes = {ui::VKEY_CONTROL, ui::VKEY_SHIFT,
                               ui::VKEY_MEDIA_LAUNCH_APP1};

  return nudge_data;
}

// Creates a view containing a keyboard illustration that indicates the
// location of the keys in the screenshot keyboard shortcut.
std::unique_ptr<views::ImageView> CreateImageView() {
  auto image_view = std::make_unique<views::ImageView>();
  image_view->SetImage(
      ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
          IDR_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE));
  // Rescale the image size to properly take up the width of the container.
  auto image_bounds = image_view->GetImageBounds();
  const float image_scale =
      static_cast<float>(kKeyboardImageWidth) / image_bounds.width();
  image_view->SetImageSize(
      gfx::Size(kKeyboardImageWidth, image_bounds.height() * image_scale));
  image_view->SetHorizontalAlignment(views::ImageViewBase::Alignment::kCenter);
  image_view->SetVerticalAlignment(views::ImageViewBase::Alignment::kCenter);
  image_view->SetID(VIEW_ID_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE);
  return image_view;
}

// Creates a view containing a keyboard shortcut view and a keyboard
// illustration. To be used as the middle content in a
// `SystemDialogDelegateView`.
std::unique_ptr<views::TableLayoutView> CreateContentView() {
  // Use a vertical table with two rows, so we can choose which cells to
  // left-align.
  auto content_view = std::make_unique<views::TableLayoutView>();
  content_view->AddColumn(
      views::LayoutAlignment::kStretch, views::LayoutAlignment::kStretch,
      /*horizontal_resize=*/1.0f, views::TableLayout::ColumnSize::kUsePreferred,
      /*fixed_width=*/0, /*min_width=*/0);
  content_view->AddRows(1, views::TableLayout::kFixedSize);
  content_view->AddPaddingRow(views::TableLayout::kFixedSize, kRowSpacing);
  content_view->AddRows(1, views::TableLayout::kFixedSize);
  // If the middle content of `SystemDialogDelegateView` has no top margin, it
  // will automatically insert a default content padding. We want to avoid this,
  // so we will set the margin ourselves.
  content_view->SetProperty(
      views::kMarginsKey,
      gfx::Insets::TLBR(kTitleShortcutSpacing, 0, kImageButtonSpacing, 0));

  // The shortcut view should be left-aligned with the title text.
  const std::vector<ui::KeyboardCode> key_codes{
      ui::VKEY_CONTROL, ui::VKEY_SHIFT, ui::VKEY_MEDIA_LAUNCH_APP1};
  auto* shortcut_view = content_view->AddChildView(
      std::make_unique<KeyboardShortcutView>(key_codes));
  shortcut_view->SetProperty(views::kTableHorizAlignKey,
                             views::LayoutAlignment::kStart);

  // Add the keyboard illustration below the keyboard shortcut.
  content_view->AddChildView(CreateImageView());

  return content_view;
}

// Creates a `SystemDialogDelegateView` to be used as the `WidgetDelegate` for
// the Arm 2 tutorial widget.
std::unique_ptr<SystemDialogDelegateView> CreateDialogView() {
  auto dialog = std::make_unique<SystemDialogDelegateView>();
  dialog->SetTitleText(l10n_util::GetStringUTF16(
      IDS_ASH_SCREEN_CAPTURE_EDUCATION_TUTORIAL_TITLE));
  dialog->SetAccessibleTitle(l10n_util::GetStringUTF16(
      IDS_ASH_SCREEN_CAPTURE_EDUCATION_TUTORIAL_ACCESSIBLE_TITLE));
  dialog->SetMiddleContentView(CreateContentView());
  dialog->SetMiddleContentAlignment(views::LayoutAlignment::kStretch);
  // Override the title margins to be zero, as the space between the title and
  // the shortcut view has already been set by the `content_view` margins.
  dialog->SetTitleMargins(gfx::Insets());
  dialog->SetAcceptButtonVisible(false);
  dialog->SetModalType(ui::mojom::ModalType::kSystem);
  return dialog;
}

}  // namespace

CaptureModeEducationController::CaptureModeEducationController() = default;

CaptureModeEducationController::~CaptureModeEducationController() = default;

// static
void CaptureModeEducationController::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(prefs::kCaptureModeEducationShownCount, 0);
  registry->RegisterTimePref(prefs::kCaptureModeEducationLastShown,
                             base::Time());
}

// static
bool CaptureModeEducationController::IsArm1ShortcutNudgeEnabled() {
  return features::IsCaptureModeEducationEnabled() &&
         features::kCaptureModeEducationParam.Get() ==
             features::CaptureModeEducationParam::kShortcutNudge;
}

// static
bool CaptureModeEducationController::IsArm2ShortcutTutorialEnabled() {
  return features::IsCaptureModeEducationEnabled() &&
         features::kCaptureModeEducationParam.Get() ==
             features::CaptureModeEducationParam::kShortcutTutorial;
}

// static
bool CaptureModeEducationController::IsArm3QuickSettingsNudgeEnabled() {
  return features::IsCaptureModeEducationEnabled() &&
         features::kCaptureModeEducationParam.Get() ==
             features::CaptureModeEducationParam::kQuickSettingsNudge;
}

void CaptureModeEducationController::MaybeShowEducation() {
  // We don't want to show the nudge if the user is not signed in yet or is on
  // the lock screen.
  if (Shell::Get()->session_controller()->IsUserSessionBlocked()) {
    return;
  }

  // Check the feature here so we only record data for users that could have hit
  // the education nudge.
  if (!features::IsCaptureModeEducationEnabled()) {
    return;
  }

  auto* pref_service = GetPrefService();
  CHECK(pref_service);

  if (!(features::IsCaptureModeEducationBypassLimitsEnabled() ||
        skip_prefs_for_test_)) {
    const int shown_count =
        pref_service->GetInteger(prefs::kCaptureModeEducationShownCount);
    const base::Time last_shown_time =
        pref_service->GetTime(prefs::kCaptureModeEducationLastShown);

    // Do not show the nudge more than three times, or if it has already been
    // shown in the past 24 hours.
    const base::Time now = GetTime();
    if ((shown_count >= kNudgeMaxShownCount) ||
        ((now - last_shown_time) < kNudgeTimeBetweenShown)) {
      return;
    }

    // Update the preferences since a form of education must have been shown, as
    // `this` is only created if the feature flag is enabled.
    pref_service->SetInteger(prefs::kCaptureModeEducationShownCount,
                             shown_count + 1);
    pref_service->SetTime(prefs::kCaptureModeEducationLastShown, now);
  }

  CloseAllEducationNudgesAndTutorials();

  if (IsArm1ShortcutNudgeEnabled()) {
    ShowShortcutNudge();
  }

  if (IsArm2ShortcutTutorialEnabled()) {
    ShowTutorialNudge();
  }

  if (IsArm3QuickSettingsNudgeEnabled()) {
    ShowQuickSettingsNudge();
  }
}

void CaptureModeEducationController::CloseAllEducationNudgesAndTutorials() {
  AnchoredNudgeManager::Get()->Cancel(kCaptureModeNudgeId);
  tutorial_widget_.reset();
}

// static
void CaptureModeEducationController::SetOverrideClockForTesting(
    base::Clock* test_clock) {
  g_clock_override = test_clock;
}

void CaptureModeEducationController::ShowShortcutNudge() {
  AnchoredNudgeData nudge_data =
      CreateBaseNudgeData(NudgeCatalogName::kCaptureModeEducationShortcutNudge);

  AnchoredNudgeManager::Get()->Show(nudge_data);
}

void CaptureModeEducationController::ShowTutorialNudge() {
  AnchoredNudgeData nudge_data = CreateBaseNudgeData(
      NudgeCatalogName::kCaptureModeEducationShortcutTutorial);

  nudge_data.primary_button_text =
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_BUTTON);
  nudge_data.primary_button_callback = base::BindRepeating(
      &CaptureModeEducationController::OnShowMeHowButtonPressed,
      weak_ptr_factory_.GetWeakPtr());

  AnchoredNudgeManager::Get()->Show(nudge_data);
}

void CaptureModeEducationController::ShowQuickSettingsNudge() {
  AnchoredNudgeData nudge_data(
      kCaptureModeNudgeId,
      NudgeCatalogName::kCaptureModeEducationQuickSettingsNudge,
      l10n_util::GetStringUTF16(
          IDS_ASH_SCREEN_CAPTURE_EDUCATION_SETTINGS_NUDGE_LABEL));

  nudge_data.image_model =
      ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
          IDR_SCREEN_CAPTURE_EDUCATION_NUDGE_IMAGE);
  nudge_data.fill_image_size = true;
  nudge_data.SetAnchorView(
      RootWindowController::ForWindow(Shell::GetRootWindowForNewWindows())
          ->shelf()
          ->status_area_widget()
          ->unified_system_tray());

  AnchoredNudgeManager::Get()->Show(nudge_data);
}

void CaptureModeEducationController::CreateAndShowTutorialDialog() {
  // As we are creating a system modal dialog, it will automatically be parented
  // to `kShellWindowId_SystemModalContainer`.
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.delegate = CreateDialogView().release();
  params.name = "CaptureModeEducationTutorialWidget";
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  tutorial_widget_ = std::make_unique<views::Widget>();
  tutorial_widget_->Init(std::move(params));
  tutorial_widget_->Show();
}

void CaptureModeEducationController::OnShowMeHowButtonPressed() {
  CHECK(!tutorial_widget_);
  CreateAndShowTutorialDialog();
}

}  // namespace ash