#include "ash/clipboard/clipboard_nudge_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/clipboard_nudge_constants.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
namespace ash {
namespace {
base::Clock* g_clock_override = nullptr;
constexpr char kCappedNudgeLastTimeShown[] = "last_time_shown";
constexpr char kShownCountDuplicateCopyNudge[] =
"shown_count_duplicate_copy_nudge";
constexpr char kShownCountOnboardingNudge[] = "shown_count";
constexpr char kClipboardNudgeId[] = "ClipboardContextualNudge";
constexpr int kMaxSeconds = 61;
int GetBodyTextStringId(ClipboardNudgeType nudge_type) {
switch (nudge_type) {
case ClipboardNudgeType::kDuplicateCopyNudge:
return IDS_ASH_MULTIPASTE_DUPLICATE_COPY_NUDGE;
case ClipboardNudgeType::kOnboardingNudge:
return IDS_ASH_MULTIPASTE_CONTEXTUAL_NUDGE;
case ClipboardNudgeType::kScreenshotNotificationNudge:
return IDS_ASH_MULTIPASTE_SCREENSHOT_NOTIFICATION_NUDGE;
case ClipboardNudgeType::kZeroStateNudge:
return IDS_ASH_MULTIPASTE_ZERO_STATE_CONTEXTUAL_NUDGE;
}
}
NudgeCatalogName GetCatalogName(ClipboardNudgeType type) {
switch (type) {
case kOnboardingNudge:
return NudgeCatalogName::kClipboardHistoryOnboarding;
case kZeroStateNudge:
return NudgeCatalogName::kClipboardHistoryZeroState;
case kScreenshotNotificationNudge:
NOTREACHED();
case kDuplicateCopyNudge:
return NudgeCatalogName::kClipboardHistoryDuplicateCopy;
}
NOTREACHED();
}
ui::ImageModel GetImage(ClipboardNudgeType type) {
switch (type) {
case kDuplicateCopyNudge:
case kOnboardingNudge:
return ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
IDR_CLIPBOARD_NUDGE_COPIED_IMAGE);
case kScreenshotNotificationNudge:
case kZeroStateNudge:
return ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
IDR_CLIPBOARD_NUDGE_SELECT_IMAGE);
}
}
base::Time GetTime() {
return g_clock_override ? g_clock_override->Now() : base::Time::Now();
}
bool IsCappedNudge(ClipboardNudgeType type) {
switch (type) {
case kOnboardingNudge:
case kDuplicateCopyNudge:
return true;
case kScreenshotNotificationNudge:
case kZeroStateNudge:
return false;
}
}
const char* GetCappedNudgeShownCountPrefKey(ClipboardNudgeType type) {
CHECK(IsCappedNudge(type));
switch (type) {
case kOnboardingNudge:
return kShownCountOnboardingNudge;
case kDuplicateCopyNudge:
return kShownCountDuplicateCopyNudge;
case kScreenshotNotificationNudge:
case kZeroStateNudge:
NOTREACHED();
}
}
int GetCappedNudgeShownCount(const PrefService& prefs,
ClipboardNudgeType type) {
return prefs.GetDict(prefs::kMultipasteNudges)
.FindInt(GetCappedNudgeShownCountPrefKey(type))
.value_or(0);
}
base::Time GetCappedNudgeLastShownTime(const PrefService& prefs) {
const std::optional<base::Time> last_shown_time = base::ValueToTime(
prefs.GetDict(prefs::kMultipasteNudges).Find(kCappedNudgeLastTimeShown));
return last_shown_time.value_or(base::Time());
}
bool ShouldShowCappedNudge(const PrefService& prefs, ClipboardNudgeType type) {
if (GetCappedNudgeShownCount(prefs, type) >= kCappedNudgeShownLimit) {
return false;
}
const base::Time last_shown_time = GetCappedNudgeLastShownTime(prefs);
return last_shown_time.is_null() ||
GetTime() - last_shown_time > kCappedNudgeMinInterval;
}
}
constexpr ClipboardNudgeController::NudgeTimeDeltaRecorder::
NudgeTimeDeltaRecorder(ClipboardNudgeType nudge_type)
: nudge_type_(nudge_type) {}
ClipboardNudgeController::NudgeTimeDeltaRecorder::~NudgeTimeDeltaRecorder() {
Reset();
}
void ClipboardNudgeController::NudgeTimeDeltaRecorder::OnNudgeShown() {
Reset();
nudge_shown_time_ = GetTime();
}
void ClipboardNudgeController::NudgeTimeDeltaRecorder::
OnClipboardHistoryPasted() {
if (ShouldRecordClipboardHistoryPasteTimeDelta()) {
base::UmaHistogramExactLinear(
GetClipboardHistoryPasteTimeDeltaHistogram(nudge_type_),
GetTimeSinceNudgeShown().InSeconds(), kMaxSeconds);
has_recorded_paste_ = true;
}
}
void ClipboardNudgeController::NudgeTimeDeltaRecorder::
OnClipboardHistoryMenuShown() {
if (ShouldRecordMenuOpenTimeDelta()) {
base::UmaHistogramExactLinear(GetMenuOpenTimeDeltaHistogram(nudge_type_),
GetTimeSinceNudgeShown().InSeconds(),
kMaxSeconds);
has_recorded_menu_shown_ = true;
}
}
void ClipboardNudgeController::NudgeTimeDeltaRecorder::Reset() {
if (ShouldRecordMenuOpenTimeDelta()) {
base::UmaHistogramExactLinear(GetMenuOpenTimeDeltaHistogram(nudge_type_),
kMaxSeconds, kMaxSeconds);
}
if (ShouldRecordClipboardHistoryPasteTimeDelta()) {
base::UmaHistogramExactLinear(
GetClipboardHistoryPasteTimeDeltaHistogram(nudge_type_), kMaxSeconds,
kMaxSeconds);
}
nudge_shown_time_ = base::Time();
has_recorded_menu_shown_ = false;
has_recorded_paste_ = false;
}
base::TimeDelta
ClipboardNudgeController::NudgeTimeDeltaRecorder::GetTimeSinceNudgeShown()
const {
CHECK(!nudge_shown_time_.is_null());
return GetTime() - nudge_shown_time_;
}
bool ClipboardNudgeController::NudgeTimeDeltaRecorder::
ShouldRecordClipboardHistoryPasteTimeDelta() const {
return !nudge_shown_time_.is_null() && !has_recorded_paste_;
}
bool ClipboardNudgeController::NudgeTimeDeltaRecorder::
ShouldRecordMenuOpenTimeDelta() const {
return !nudge_shown_time_.is_null() && !has_recorded_menu_shown_;
}
ClipboardNudgeController::ClipboardNudgeController(
ClipboardHistory* clipboard_history) {
clipboard_history_observation_.Observe(clipboard_history);
clipboard_history_controller_observation_.Observe(
ClipboardHistoryController::Get());
clipboard_monitor_observation_.Observe(ui::ClipboardMonitor::GetInstance());
}
ClipboardNudgeController::~ClipboardNudgeController() = default;
void ClipboardNudgeController::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kMultipasteNudges);
}
void ClipboardNudgeController::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item,
bool is_duplicate) {
const PrefService* const prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!prefs) {
return;
}
if (ShouldShowCappedNudge(*prefs, ClipboardNudgeType::kOnboardingNudge)) {
switch (onboarding_state_) {
case OnboardingState::kInit:
onboarding_state_ = OnboardingState::kFirstCopy;
break;
case OnboardingState::kFirstPaste:
onboarding_state_ = OnboardingState::kSecondCopy;
break;
case OnboardingState::kFirstCopy:
case OnboardingState::kSecondCopy:
break;
}
}
if (is_duplicate &&
ShouldShowCappedNudge(*prefs, ClipboardNudgeType::kDuplicateCopyNudge)) {
ShowNudge(ClipboardNudgeType::kDuplicateCopyNudge);
}
}
std::optional<base::Time> ClipboardNudgeController::GetNudgeLastTimeShown()
const {
const base::Time& nudge_last_time_shown =
std::ranges::max(
{&duplicate_copy_nudge_recorder_, &onboarding_nudge_recorder_,
&screenshot_nudge_recorder_, &zero_state_nudge_recorder_},
{}, &NudgeTimeDeltaRecorder::nudge_shown_time)
->nudge_shown_time();
return nudge_last_time_shown.is_null()
? std::nullopt
: std::make_optional(nudge_last_time_shown);
}
void ClipboardNudgeController::MarkScreenshotNotificationShown() {
base::UmaHistogramBoolean(kClipboardHistoryScreenshotNotificationShowCount,
true);
screenshot_nudge_recorder_.OnNudgeShown();
}
void ClipboardNudgeController::OnClipboardDataRead() {
if (const PrefService* const prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
clipboard_history_util::IsEnabledInCurrentMode() && prefs &&
ShouldShowCappedNudge(*prefs, ClipboardNudgeType::kOnboardingNudge)) {
switch (onboarding_state_) {
case OnboardingState::kInit:
return;
case OnboardingState::kFirstCopy:
onboarding_state_ = OnboardingState::kFirstPaste;
last_paste_timestamp_ = GetTime();
return;
case OnboardingState::kFirstPaste:
last_paste_timestamp_ = GetTime();
return;
case OnboardingState::kSecondCopy:
if (GetTime() - last_paste_timestamp_ < kMaxTimeBetweenPaste) {
ShowNudge(ClipboardNudgeType::kOnboardingNudge);
} else {
onboarding_state_ = OnboardingState::kFirstPaste;
last_paste_timestamp_ = GetTime();
}
return;
}
}
}
void ClipboardNudgeController::OnClipboardHistoryMenuShown(
crosapi::mojom::ClipboardHistoryControllerShowSource show_source) {
if (show_source !=
crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator) {
return;
}
onboarding_nudge_recorder_.OnClipboardHistoryMenuShown();
zero_state_nudge_recorder_.OnClipboardHistoryMenuShown();
screenshot_nudge_recorder_.OnClipboardHistoryMenuShown();
AnchoredNudgeManager::Get()->MaybeRecordNudgeAction(
NudgeCatalogName::kClipboardHistoryOnboarding);
AnchoredNudgeManager::Get()->MaybeRecordNudgeAction(
NudgeCatalogName::kClipboardHistoryZeroState);
duplicate_copy_nudge_recorder_.OnClipboardHistoryMenuShown();
AnchoredNudgeManager::Get()->MaybeRecordNudgeAction(
NudgeCatalogName::kClipboardHistoryDuplicateCopy);
}
void ClipboardNudgeController::OnClipboardHistoryPasted() {
onboarding_nudge_recorder_.OnClipboardHistoryPasted();
zero_state_nudge_recorder_.OnClipboardHistoryPasted();
screenshot_nudge_recorder_.OnClipboardHistoryPasted();
duplicate_copy_nudge_recorder_.OnClipboardHistoryPasted();
}
void ClipboardNudgeController::ShowNudge(ClipboardNudgeType nudge_type) {
current_nudge_type_ = nudge_type;
const std::u16string shortcut_key =
clipboard_history_util::GetShortcutKeyName();
const std::u16string body_text = l10n_util::GetStringFUTF16(
GetBodyTextStringId(current_nudge_type_), shortcut_key);
AnchoredNudgeData nudge_data(kClipboardNudgeId,
GetCatalogName(current_nudge_type_), body_text);
nudge_data.image_model = GetImage(current_nudge_type_);
AnchoredNudgeManager::Get()->Show(nudge_data);
switch (nudge_type) {
case ClipboardNudgeType::kOnboardingNudge:
onboarding_nudge_recorder_.OnNudgeShown();
base::UmaHistogramBoolean(kClipboardHistoryOnboardingNudgeShowCount,
true);
break;
case ClipboardNudgeType::kZeroStateNudge:
zero_state_nudge_recorder_.OnNudgeShown();
base::UmaHistogramBoolean(kClipboardHistoryZeroStateNudgeShowCount, true);
break;
case ClipboardNudgeType::kScreenshotNotificationNudge:
NOTREACHED();
case ClipboardNudgeType::kDuplicateCopyNudge:
duplicate_copy_nudge_recorder_.OnNudgeShown();
base::UmaHistogramBoolean(kClipboardHistoryDuplicateCopyNudgeShowCount,
true);
break;
}
onboarding_state_ = OnboardingState::kInit;
if (PrefService* const prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
prefs && IsCappedNudge(nudge_type)) {
ScopedDictPrefUpdate update(prefs, prefs::kMultipasteNudges);
update->Set(GetCappedNudgeShownCountPrefKey(nudge_type),
GetCappedNudgeShownCount(*prefs, nudge_type) + 1);
update->Set(kCappedNudgeLastTimeShown, base::TimeToValue(GetTime()));
}
}
void ClipboardNudgeController::OverrideClockForTesting(
base::Clock* test_clock) {
DCHECK(!g_clock_override);
g_clock_override = test_clock;
}
void ClipboardNudgeController::ClearClockOverrideForTesting() {
DCHECK(g_clock_override);
g_clock_override = nullptr;
}
}