1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/clipboard/clipboard_nudge_controller.h"
6 
7 #include "ash/clipboard/clipboard_history_item.h"
8 #include "ash/clipboard/clipboard_history_util.h"
9 #include "ash/clipboard/clipboard_nudge.h"
10 #include "ash/clipboard/clipboard_nudge_constants.h"
11 #include "ash/public/cpp/ash_pref_names.h"
12 #include "ash/session/session_controller_impl.h"
13 #include "ash/shell.h"
14 #include "base/logging.h"
15 #include "base/metrics/histogram_functions.h"
16 #include "base/metrics/histogram_macros.h"
17 #include "base/no_destructor.h"
18 #include "base/util/values/values_util.h"
19 #include "chromeos/constants/chromeos_features.h"
20 #include "components/prefs/pref_registry_simple.h"
21 #include "components/prefs/pref_service.h"
22 #include "components/prefs/scoped_user_pref_update.h"
23 #include "ui/base/clipboard/clipboard_monitor.h"
24 #include "ui/compositor/layer_animation_observer.h"
25 #include "ui/compositor/scoped_layer_animation_settings.h"
26 
27 namespace {
28 
29 // Keys for tooltip sub-preferences for shown count and last time shown.
30 constexpr char kShownCount[] = "shown_count";
31 constexpr char kLastTimeShown[] = "last_time_shown";
32 
33 // The maximum number of 1 second buckets used to record the time between
34 // showing the nudge and recording the feature being opened/used.
35 constexpr int kBucketCount = 61;
36 
37 // A class for observing the clipboard nudge fade out animation. Once the fade
38 // out animation is complete the clipboard nudge will be destroyed.
39 class ImplicitNudgeHideAnimationObserver
40     : public ui::ImplicitAnimationObserver {
41  public:
ImplicitNudgeHideAnimationObserver(std::unique_ptr<ash::ClipboardNudge> nudge)42   explicit ImplicitNudgeHideAnimationObserver(
43       std::unique_ptr<ash::ClipboardNudge> nudge)
44       : nudge_(std::move(nudge)) {}
45   ImplicitNudgeHideAnimationObserver(
46       const ImplicitNudgeHideAnimationObserver&) = delete;
47   ImplicitNudgeHideAnimationObserver& operator=(
48       const ImplicitNudgeHideAnimationObserver&) = delete;
~ImplicitNudgeHideAnimationObserver()49   ~ImplicitNudgeHideAnimationObserver() override {
50     StopObservingImplicitAnimations();
51     nudge_->Close();
52   }
53 
54   // ui::ImplicitAnimationObserver:
OnImplicitAnimationsCompleted()55   void OnImplicitAnimationsCompleted() override { delete this; }
56 
57  private:
58   std::unique_ptr<ash::ClipboardNudge> nudge_;
59 };
60 
61 }  // namespace
62 
63 namespace ash {
64 
ClipboardNudgeController(ClipboardHistory * clipboard_history,ClipboardHistoryControllerImpl * clipboard_history_controller)65 ClipboardNudgeController::ClipboardNudgeController(
66     ClipboardHistory* clipboard_history,
67     ClipboardHistoryControllerImpl* clipboard_history_controller)
68     : clipboard_history_(clipboard_history),
69       clipboard_history_controller_(clipboard_history_controller) {
70   clipboard_history_->AddObserver(this);
71   clipboard_history_controller_->AddObserver(this);
72   ui::ClipboardMonitor::GetInstance()->AddObserver(this);
73   if (chromeos::features::IsClipboardHistoryNudgeSessionResetEnabled())
74     Shell::Get()->session_controller()->AddObserver(this);
75 }
76 
~ClipboardNudgeController()77 ClipboardNudgeController::~ClipboardNudgeController() {
78   clipboard_history_->RemoveObserver(this);
79   clipboard_history_controller_->RemoveObserver(this);
80   ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);
81   if (chromeos::features::IsClipboardHistoryNudgeSessionResetEnabled())
82     Shell::Get()->session_controller()->RemoveObserver(this);
83 }
84 
85 // static
RegisterProfilePrefs(PrefRegistrySimple * registry)86 void ClipboardNudgeController::RegisterProfilePrefs(
87     PrefRegistrySimple* registry) {
88   registry->RegisterDictionaryPref(prefs::kMultipasteNudges);
89 }
90 
OnClipboardHistoryItemAdded(const ClipboardHistoryItem & item)91 void ClipboardNudgeController::OnClipboardHistoryItemAdded(
92     const ClipboardHistoryItem& item) {
93   PrefService* prefs =
94       Shell::Get()->session_controller()->GetLastActiveUserPrefService();
95   if (!ShouldShowNudge(prefs))
96     return;
97 
98   switch (clipboard_state_) {
99     case ClipboardState::kInit:
100       clipboard_state_ = ClipboardState::kFirstCopy;
101       return;
102     case ClipboardState::kFirstPaste:
103       clipboard_state_ = ClipboardState::kSecondCopy;
104       return;
105     case ClipboardState::kFirstCopy:
106     case ClipboardState::kSecondCopy:
107     case ClipboardState::kShouldShowNudge:
108       return;
109   }
110 }
111 
OnClipboardDataRead()112 void ClipboardNudgeController::OnClipboardDataRead() {
113   PrefService* prefs =
114       Shell::Get()->session_controller()->GetLastActiveUserPrefService();
115   if (!ClipboardHistoryUtil::IsEnabledInCurrentMode() ||
116       !ShouldShowNudge(prefs)) {
117     return;
118   }
119 
120   switch (clipboard_state_) {
121     case ClipboardState::kFirstCopy:
122       clipboard_state_ = ClipboardState::kFirstPaste;
123       last_paste_timestamp_ = GetTime();
124       return;
125     case ClipboardState::kFirstPaste:
126       // Subsequent pastes should reset the timestamp.
127       last_paste_timestamp_ = GetTime();
128       return;
129     case ClipboardState::kSecondCopy:
130       if (GetTime() - last_paste_timestamp_ < kMaxTimeBetweenPaste) {
131         ShowNudge();
132         HandleNudgeShown();
133       } else {
134         // ClipboardState should be reset to kFirstPaste when timed out.
135         clipboard_state_ = ClipboardState::kFirstPaste;
136         last_paste_timestamp_ = GetTime();
137       }
138       return;
139     case ClipboardState::kInit:
140     case ClipboardState::kShouldShowNudge:
141       return;
142   }
143 }
144 
OnActiveUserPrefServiceChanged(PrefService * prefs)145 void ClipboardNudgeController::OnActiveUserPrefServiceChanged(
146     PrefService* prefs) {
147   // Reset the nudge prefs so that the nudge can be shown again.
148   DictionaryPrefUpdate update(prefs, prefs::kMultipasteNudges);
149   update->SetIntPath(kShownCount, 0);
150   update->SetPath(kLastTimeShown, util::TimeToValue(base::Time()));
151 }
152 
ShowNudge()153 void ClipboardNudgeController::ShowNudge() {
154   // Create and show the nudge.
155   nudge_ = std::make_unique<ClipboardNudge>();
156   StartFadeAnimation(/*show=*/true);
157 
158   // Start a timer to close the nudge after a set amount of time.
159   hide_nudge_timer_.Start(FROM_HERE, kNudgeShowTime,
160                           base::BindOnce(&ClipboardNudgeController::HideNudge,
161                                          weak_ptr_factory_.GetWeakPtr()));
162   last_shown_time_ = GetTime();
163 
164   // Tracks the number of times the ClipboardHistory nudge is shown.
165   // This allows us to understand the conversion rate of showing a nudge to
166   // a user opening and then using the clipboard history feature.
167   base::UmaHistogramExactLinear(
168       "Ash.ClipboardHistory.ContextualNudge.ShownCount", 1, 1);
169 }
170 
HideNudge()171 void ClipboardNudgeController::HideNudge() {
172   StartFadeAnimation(/*show=*/false);
173 }
174 
StartFadeAnimation(bool show)175 void ClipboardNudgeController::StartFadeAnimation(bool show) {
176   ui::Layer* layer = nudge_->widget()->GetLayer();
177   gfx::Rect widget_bounds = layer->bounds();
178 
179   gfx::Transform scaled_nudge_transform;
180   float x_offset =
181       widget_bounds.width() * (1.0f - kNudgeFadeAnimationScale) / 2.0f;
182   float y_offset =
183       widget_bounds.height() * (1.0f - kNudgeFadeAnimationScale) / 2.0f;
184   scaled_nudge_transform.Translate(x_offset, y_offset);
185   scaled_nudge_transform.Scale(kNudgeFadeAnimationScale,
186                                kNudgeFadeAnimationScale);
187 
188   layer->SetOpacity(show ? 0.0f : 1.0f);
189   layer->SetTransform(show ? scaled_nudge_transform : gfx::Transform());
190 
191   {
192     // Perform the scaling animation on the clipboard nudge.
193     ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
194     settings.SetTransitionDuration(kNudgeFadeAnimationTime);
195     settings.SetTweenType(kNudgeFadeScalingAnimationTweenType);
196     layer->SetTransform(show ? gfx::Transform() : scaled_nudge_transform);
197   }
198   {
199     // Perform the opacity animation on the clipboard nudge.
200     ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
201     settings.SetTransitionDuration(kNudgeFadeAnimationTime);
202     settings.SetTweenType(kNudgeFadeOpacityAnimationTweenType);
203     layer->SetOpacity(show ? 1.0f : 0.0f);
204     if (!show) {
205       settings.AddObserver(
206           new ImplicitNudgeHideAnimationObserver(std::move(nudge_)));
207     }
208   }
209 }
210 
HandleNudgeShown()211 void ClipboardNudgeController::HandleNudgeShown() {
212   clipboard_state_ = ClipboardState::kInit;
213   PrefService* prefs =
214       Shell::Get()->session_controller()->GetLastActiveUserPrefService();
215   const int shown_count = GetShownCount(prefs);
216   DictionaryPrefUpdate update(prefs, prefs::kMultipasteNudges);
217   update->SetIntPath(kShownCount, shown_count + 1);
218   update->SetPath(kLastTimeShown, util::TimeToValue(GetTime()));
219 }
220 
OnClipboardHistoryMenuShown()221 void ClipboardNudgeController::OnClipboardHistoryMenuShown() {
222   if (last_shown_time_.is_null())
223     return;
224   base::TimeDelta time_since_shown = GetTime() - last_shown_time_;
225 
226   // Tracks the amount of time between showing the user a nudge and the user
227   // opening the ClipboardHistory menu.
228   base::UmaHistogramExactLinear(
229       "Ash.ClipboardHistory.ContextualNudge.NudgeToFeatureOpenTime",
230       time_since_shown.InSeconds(), kBucketCount);
231 }
232 
OnClipboardHistoryPasted()233 void ClipboardNudgeController::OnClipboardHistoryPasted() {
234   if (last_shown_time_.is_null())
235     return;
236   base::TimeDelta time_since_shown = GetTime() - last_shown_time_;
237 
238   // Tracks the amount of time between showing the user a nudge and the user
239   // using the ClipboardHistory feature.
240   base::UmaHistogramExactLinear(
241       "Ash.ClipboardHistory.ContextualNudge.NudgeToFeatureUseTime",
242       time_since_shown.InSeconds(), kBucketCount);
243 }
244 
OverrideClockForTesting(base::Clock * test_clock)245 void ClipboardNudgeController::OverrideClockForTesting(
246     base::Clock* test_clock) {
247   DCHECK(!g_clock_override);
248   g_clock_override = test_clock;
249 }
250 
ClearClockOverrideForTesting()251 void ClipboardNudgeController::ClearClockOverrideForTesting() {
252   DCHECK(g_clock_override);
253   g_clock_override = nullptr;
254 }
255 
GetClipboardStateForTesting()256 const ClipboardState& ClipboardNudgeController::GetClipboardStateForTesting() {
257   return clipboard_state_;
258 }
259 
GetShownCount(PrefService * prefs)260 int ClipboardNudgeController::GetShownCount(PrefService* prefs) {
261   const base::DictionaryValue* dictionary =
262       prefs->GetDictionary(prefs::kMultipasteNudges);
263   if (!dictionary)
264     return 0;
265   return dictionary->FindIntPath(kShownCount).value_or(0);
266 }
267 
GetLastShownTime(PrefService * prefs)268 base::Time ClipboardNudgeController::GetLastShownTime(PrefService* prefs) {
269   const base::DictionaryValue* dictionary =
270       prefs->GetDictionary(prefs::kMultipasteNudges);
271   if (!dictionary)
272     return base::Time();
273   base::Optional<base::Time> last_shown_time =
274       util::ValueToTime(dictionary->FindPath(kLastTimeShown));
275   return last_shown_time.value_or(base::Time());
276 }
277 
ShouldShowNudge(PrefService * prefs)278 bool ClipboardNudgeController::ShouldShowNudge(PrefService* prefs) {
279   if (!prefs)
280     return false;
281   int nudge_shown_count = GetShownCount(prefs);
282   base::Time last_shown_time = GetLastShownTime(prefs);
283   // We should not show more nudges after hitting the limit.
284   if (nudge_shown_count >= kNotificationLimit)
285     return false;
286   // If the nudge has yet to be shown, we should return true.
287   if (last_shown_time.is_null())
288     return true;
289 
290   // We should show the nudge if enough time has passed since the nudge was last
291   // shown.
292   return base::Time::Now() - last_shown_time > kMinInterval;
293 }
294 
GetTime()295 base::Time ClipboardNudgeController::GetTime() {
296   if (g_clock_override)
297     return g_clock_override->Now();
298   return base::Time::Now();
299 }
300 
301 }  // namespace ash
302