blob: 99489ec6475afa9e1701436aabe20a8a979b7901 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/clipboard/clipboard_nudge_constants.h"
#include "ash/public/cpp/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/util/values/values_util.h"
#include "chromeos/constants/chromeos_features.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/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
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 kBucketCount = 61;
// A class for observing the clipboard nudge fade out animation. Once the fade
// out animation is complete the clipboard nudge will be destroyed.
class ImplicitNudgeHideAnimationObserver
: public ui::ImplicitAnimationObserver {
public:
explicit ImplicitNudgeHideAnimationObserver(
std::unique_ptr<ash::ClipboardNudge> nudge)
: nudge_(std::move(nudge)) {}
ImplicitNudgeHideAnimationObserver(
const ImplicitNudgeHideAnimationObserver&) = delete;
ImplicitNudgeHideAnimationObserver& operator=(
const ImplicitNudgeHideAnimationObserver&) = delete;
~ImplicitNudgeHideAnimationObserver() override {
StopObservingImplicitAnimations();
nudge_->Close();
}
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override { delete this; }
private:
std::unique_ptr<ash::ClipboardNudge> nudge_;
};
} // namespace
namespace ash {
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 (chromeos::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 (chromeos::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::OnClipboardDataRead() {
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
if (!ClipboardHistoryUtil::IsEnabledInCurrentMode() ||
!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();
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.
DictionaryPrefUpdate update(prefs, prefs::kMultipasteNudges);
update->SetIntPath(kShownCount, 0);
update->SetPath(kLastTimeShown, util::TimeToValue(base::Time()));
}
void ClipboardNudgeController::ShowNudge() {
// Create and show the nudge.
nudge_ = std::make_unique<ClipboardNudge>();
StartFadeAnimation(/*show=*/true);
// Start a timer to close the nudge after a set amount of time.
hide_nudge_timer_.Start(FROM_HERE, kNudgeShowTime,
base::BindOnce(&ClipboardNudgeController::HideNudge,
weak_ptr_factory_.GetWeakPtr()));
last_shown_time_ = GetTime();
// 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.
base::UmaHistogramExactLinear(
"Ash.ClipboardHistory.ContextualNudge.ShownCount", 1, 1);
}
void ClipboardNudgeController::HideNudge() {
StartFadeAnimation(/*show=*/false);
}
void ClipboardNudgeController::StartFadeAnimation(bool show) {
ui::Layer* layer = nudge_->widget()->GetLayer();
gfx::Rect widget_bounds = layer->bounds();
gfx::Transform scaled_nudge_transform;
float x_offset =
widget_bounds.width() * (1.0f - kNudgeFadeAnimationScale) / 2.0f;
float y_offset =
widget_bounds.height() * (1.0f - kNudgeFadeAnimationScale) / 2.0f;
scaled_nudge_transform.Translate(x_offset, y_offset);
scaled_nudge_transform.Scale(kNudgeFadeAnimationScale,
kNudgeFadeAnimationScale);
layer->SetOpacity(show ? 0.0f : 1.0f);
layer->SetTransform(show ? scaled_nudge_transform : gfx::Transform());
{
// Perform the scaling animation on the clipboard nudge.
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTransitionDuration(kNudgeFadeAnimationTime);
settings.SetTweenType(kNudgeFadeScalingAnimationTweenType);
layer->SetTransform(show ? gfx::Transform() : scaled_nudge_transform);
}
{
// Perform the opacity animation on the clipboard nudge.
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTransitionDuration(kNudgeFadeAnimationTime);
settings.SetTweenType(kNudgeFadeOpacityAnimationTweenType);
layer->SetOpacity(show ? 1.0f : 0.0f);
if (!show) {
settings.AddObserver(
new ImplicitNudgeHideAnimationObserver(std::move(nudge_)));
}
}
}
void ClipboardNudgeController::HandleNudgeShown() {
clipboard_state_ = ClipboardState::kInit;
PrefService* prefs =
Shell::Get()->session_controller()->GetLastActiveUserPrefService();
const int shown_count = GetShownCount(prefs);
DictionaryPrefUpdate update(prefs, prefs::kMultipasteNudges);
update->SetIntPath(kShownCount, shown_count + 1);
update->SetPath(kLastTimeShown, util::TimeToValue(GetTime()));
}
void ClipboardNudgeController::OnClipboardHistoryMenuShown() {
if (last_shown_time_.is_null())
return;
base::TimeDelta time_since_shown = GetTime() - last_shown_time_;
// Tracks the amount of time between showing the user a nudge and the user
// opening the ClipboardHistory menu.
base::UmaHistogramExactLinear(
"Ash.ClipboardHistory.ContextualNudge.NudgeToFeatureOpenTime",
time_since_shown.InSeconds(), kBucketCount);
}
void ClipboardNudgeController::OnClipboardHistoryPasted() {
if (last_shown_time_.is_null())
return;
base::TimeDelta time_since_shown = GetTime() - last_shown_time_;
// Tracks the amount of time between showing the user a nudge and the user
// using the ClipboardHistory feature.
base::UmaHistogramExactLinear(
"Ash.ClipboardHistory.ContextualNudge.NudgeToFeatureUseTime",
time_since_shown.InSeconds(), kBucketCount);
}
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_;
}
int ClipboardNudgeController::GetShownCount(PrefService* prefs) {
const base::DictionaryValue* dictionary =
prefs->GetDictionary(prefs::kMultipasteNudges);
if (!dictionary)
return 0;
return dictionary->FindIntPath(kShownCount).value_or(0);
}
base::Time ClipboardNudgeController::GetLastShownTime(PrefService* prefs) {
const base::DictionaryValue* dictionary =
prefs->GetDictionary(prefs::kMultipasteNudges);
if (!dictionary)
return base::Time();
base::Optional<base::Time> last_shown_time =
util::ValueToTime(dictionary->FindPath(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();
}
} // namespace ash