// 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