blob: 186951f440eadccdf6d0c7dafcd33bd56eda78ab [file] [log] [blame]
// Copyright 2020 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/clipboard/clipboard_nudge_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/clipboard_nudge.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.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"
namespace ash {
namespace {
// Keys for tooltip sub-preferences for shown count and last time shown.
constexpr char kShownCount[] = "shown_count";
constexpr char kLastTimeShown[] = "last_time_shown";
// The maximum number of 1 second buckets used to record the time between
// showing the nudge and recording the feature being opened/used.
constexpr int kMaxSeconds = 61;
// Clock that can be overridden for testing.
base::Clock* g_clock_override = nullptr;
base::Time GetTime() {
if (g_clock_override)
return g_clock_override->Now();
return base::Time::Now();
}
bool LogFeatureOpenTime(
const ClipboardNudgeController::TimeMetricHelper& metric_show_time,
const std::string& open_histogram) {
if (!metric_show_time.ShouldLogFeatureOpenTime())
return false;
base::TimeDelta time_since_shown =
metric_show_time.GetTimeSinceShown(GetTime());
// Tracks the amount of time between showing the user a nudge and
// the user opening the ClipboardHistory menu.
base::UmaHistogramExactLinear(open_histogram, time_since_shown.InSeconds(),
kMaxSeconds);
return true;
}
bool LogFeatureUsedTime(
const ClipboardNudgeController::TimeMetricHelper& metric_show_time,
const std::string& paste_histogram) {
if (!metric_show_time.ShouldLogFeatureUsedTime())
return false;
base::TimeDelta time_since_shown =
metric_show_time.GetTimeSinceShown(GetTime());
// Tracks the amount of time between showing the user a nudge and
// the user opening the ClipboardHistory menu.
base::UmaHistogramExactLinear(paste_histogram, time_since_shown.InSeconds(),
kMaxSeconds);
return true;
}
NudgeCatalogName GetCatalogName(ClipboardNudgeType type) {
switch (type) {
case kOnboardingNudge:
return NudgeCatalogName::kClipboardHistoryOnboarding;
case kZeroStateNudge:
return NudgeCatalogName::kClipboardHistoryZeroState;
case kScreenshotNotificationNudge:
NOTREACHED();
}
return NudgeCatalogName::kTestCatalogName;
}
} // namespace
ClipboardNudgeController::ClipboardNudgeController(
ClipboardHistory* clipboard_history,
ClipboardHistoryControllerImpl* clipboard_history_controller)
: clipboard_history_(clipboard_history),
clipboard_history_controller_(clipboard_history_controller) {
clipboard_history_->AddObserver(this);
clipboard_history_controller_->AddObserver(this);
ui::ClipboardMonitor::GetInstance()->AddObserver(this);
if (features::IsClipboardHistoryNudgeSessionResetEnabled())
Shell::Get()->session_controller()->AddObserver(this);
}
ClipboardNudgeController::~ClipboardNudgeController() {
clipboard_history_->RemoveObserver(this);
clipboard_history_controller_->RemoveObserver(this);
ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);
if (features::IsClipboardHistoryNudgeSessionResetEnabled())
Shell::Get()->session_controller()->RemoveObserver(this);
}
// static
void ClipboardNudgeController::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kMultipasteNudges);
}
void ClipboardNudgeController::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item,
bool is_duplicate) {
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!ShouldShowNudge(prefs))
return;
switch (clipboard_state_) {
case ClipboardState::kInit:
clipboard_state_ = ClipboardState::kFirstCopy;
return;
case ClipboardState::kFirstPaste:
clipboard_state_ = ClipboardState::kSecondCopy;
return;
case ClipboardState::kFirstCopy:
case ClipboardState::kSecondCopy:
case ClipboardState::kShouldShowNudge:
return;
}
}
void ClipboardNudgeController::MarkScreenshotNotificationShown() {
base::UmaHistogramBoolean(kScreenshotNotification_ShowCount, true);
if (screenshot_notification_last_shown_time_.ShouldLogFeatureOpenTime()) {
base::UmaHistogramExactLinear(kScreenshotNotification_OpenTime, kMaxSeconds,
kMaxSeconds);
}
if (screenshot_notification_last_shown_time_.ShouldLogFeatureUsedTime()) {
base::UmaHistogramExactLinear(kScreenshotNotification_PasteTime,
kMaxSeconds, kMaxSeconds);
}
screenshot_notification_last_shown_time_.ResetTime();
}
void ClipboardNudgeController::OnClipboardDataRead() {
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!clipboard_history_util::IsEnabledInCurrentMode() || !prefs ||
!ShouldShowNudge(prefs)) {
return;
}
switch (clipboard_state_) {
case ClipboardState::kFirstCopy:
clipboard_state_ = ClipboardState::kFirstPaste;
last_paste_timestamp_ = GetTime();
return;
case ClipboardState::kFirstPaste:
// Subsequent pastes should reset the timestamp.
last_paste_timestamp_ = GetTime();
return;
case ClipboardState::kSecondCopy:
if (GetTime() - last_paste_timestamp_ < kMaxTimeBetweenPaste) {
ShowNudge(ClipboardNudgeType::kOnboardingNudge);
HandleNudgeShown();
} else {
// ClipboardState should be reset to kFirstPaste when timed out.
clipboard_state_ = ClipboardState::kFirstPaste;
last_paste_timestamp_ = GetTime();
}
return;
case ClipboardState::kInit:
case ClipboardState::kShouldShowNudge:
return;
}
}
void ClipboardNudgeController::OnActiveUserPrefServiceChanged(
PrefService* prefs) {
// Reset the nudge prefs so that the nudge can be shown again.
ScopedDictPrefUpdate update(prefs, prefs::kMultipasteNudges);
update->Set(kShownCount, 0);
update->Set(kLastTimeShown, base::TimeToValue(base::Time()));
}
void ClipboardNudgeController::ShowNudge(ClipboardNudgeType nudge_type) {
current_nudge_type_ = nudge_type;
SystemNudgeController::ShowNudge();
// Tracks the number of times the ClipboardHistory nudge is shown.
// This allows us to understand the conversion rate of showing a nudge to
// a user opening and then using the clipboard history feature.
switch (nudge_type) {
case ClipboardNudgeType::kOnboardingNudge:
if (last_shown_time_.ShouldLogFeatureOpenTime()) {
base::UmaHistogramExactLinear(kOnboardingNudge_OpenTime, kMaxSeconds,
kMaxSeconds);
}
if (last_shown_time_.ShouldLogFeatureUsedTime()) {
base::UmaHistogramExactLinear(kOnboardingNudge_PasteTime, kMaxSeconds,
kMaxSeconds);
}
last_shown_time_.ResetTime();
base::UmaHistogramBoolean(kOnboardingNudge_ShowCount, true);
break;
case ClipboardNudgeType::kZeroStateNudge:
if (zero_state_last_shown_time_.ShouldLogFeatureOpenTime()) {
base::UmaHistogramExactLinear(kZeroStateNudge_OpenTime, kMaxSeconds,
kMaxSeconds);
}
if (zero_state_last_shown_time_.ShouldLogFeatureUsedTime()) {
base::UmaHistogramExactLinear(kZeroStateNudge_PasteTime, kMaxSeconds,
kMaxSeconds);
}
zero_state_last_shown_time_.ResetTime();
base::UmaHistogramBoolean(kZeroStateNudge_ShowCount, true);
break;
default:
NOTREACHED();
}
}
void ClipboardNudgeController::HandleNudgeShown() {
clipboard_state_ = ClipboardState::kInit;
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!prefs)
return;
const int shown_count = GetShownCount(prefs);
ScopedDictPrefUpdate update(prefs, prefs::kMultipasteNudges);
update->Set(kShownCount, shown_count + 1);
update->Set(kLastTimeShown, base::TimeToValue(GetTime()));
}
void ClipboardNudgeController::OnClipboardHistoryMenuShown(
crosapi::mojom::ClipboardHistoryControllerShowSource show_source) {
// The clipboard history nudges specifically suggest trying the Search+V
// shortcut. Opening the menu any other way should not count as the user
// responding to the nudge.
if (show_source !=
crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator) {
return;
}
if (LogFeatureOpenTime(last_shown_time_, kOnboardingNudge_OpenTime)) {
last_shown_time_.set_was_logged_as_opened();
}
if (LogFeatureOpenTime(zero_state_last_shown_time_,
kZeroStateNudge_OpenTime)) {
zero_state_last_shown_time_.set_was_logged_as_opened();
}
if (LogFeatureOpenTime(screenshot_notification_last_shown_time_,
kScreenshotNotification_OpenTime)) {
screenshot_notification_last_shown_time_.set_was_logged_as_opened();
}
// These metrics will not be recorded if the respective nudges have not been
// shown before.
SystemNudgeController::RecordNudgeAction(
NudgeCatalogName::kClipboardHistoryOnboarding);
SystemNudgeController::RecordNudgeAction(
NudgeCatalogName::kClipboardHistoryZeroState);
}
void ClipboardNudgeController::OnClipboardHistoryPasted() {
if (LogFeatureUsedTime(last_shown_time_, kOnboardingNudge_PasteTime))
last_shown_time_.set_was_logged_as_used();
if (LogFeatureUsedTime(zero_state_last_shown_time_,
kZeroStateNudge_PasteTime)) {
zero_state_last_shown_time_.set_was_logged_as_used();
}
if (LogFeatureUsedTime(screenshot_notification_last_shown_time_,
kScreenshotNotification_PasteTime)) {
screenshot_notification_last_shown_time_.set_was_logged_as_used();
}
}
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;
}
const ClipboardState& ClipboardNudgeController::GetClipboardStateForTesting() {
return clipboard_state_;
}
std::unique_ptr<SystemNudge> ClipboardNudgeController::CreateSystemNudge() {
return std::make_unique<ClipboardNudge>(current_nudge_type_,
GetCatalogName(current_nudge_type_));
}
int ClipboardNudgeController::GetShownCount(PrefService* prefs) {
const base::Value::Dict& dictionary =
prefs->GetDict(prefs::kMultipasteNudges);
return dictionary.FindInt(kShownCount).value_or(0);
}
base::Time ClipboardNudgeController::GetLastShownTime(PrefService* prefs) {
const base::Value::Dict& dictionary =
prefs->GetDict(prefs::kMultipasteNudges);
absl::optional<base::Time> last_shown_time =
base::ValueToTime(dictionary.Find(kLastTimeShown));
return last_shown_time.value_or(base::Time());
}
bool ClipboardNudgeController::ShouldShowNudge(PrefService* prefs) {
if (!prefs)
return false;
int nudge_shown_count = GetShownCount(prefs);
base::Time last_shown_time = GetLastShownTime(prefs);
// We should not show more nudges after hitting the limit.
if (nudge_shown_count >= kNotificationLimit)
return false;
// If the nudge has yet to be shown, we should return true.
if (last_shown_time.is_null())
return true;
// We should show the nudge if enough time has passed since the nudge was last
// shown.
return base::Time::Now() - last_shown_time > kMinInterval;
}
base::Time ClipboardNudgeController::GetTime() {
if (g_clock_override)
return g_clock_override->Now();
return base::Time::Now();
}
void ClipboardNudgeController::TimeMetricHelper::ResetTime() {
last_shown_time_ =
g_clock_override ? g_clock_override->Now() : base::Time::Now();
was_logged_as_opened_ = false;
was_logged_as_used_ = false;
}
bool ClipboardNudgeController::TimeMetricHelper::ShouldLogFeatureUsedTime()
const {
return !last_shown_time_.is_null() && !was_logged_as_used_;
}
bool ClipboardNudgeController::TimeMetricHelper::ShouldLogFeatureOpenTime()
const {
return !last_shown_time_.is_null() && !was_logged_as_opened_;
}
base::TimeDelta ClipboardNudgeController::TimeMetricHelper::GetTimeSinceShown(
base::Time current_time) const {
return current_time - last_shown_time_;
}
} // namespace ash