1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
ash / wm / desks / desk_animation_impl.cc [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/wm/desks/desk_animation_impl.h"
#include "ash/app_menu/menu_util.h"
#include "ash/shell.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/utils/haptics_util.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/events/devices/haptic_touchpad_effects.h"
namespace ash {
namespace {
constexpr char kDeskActivationLatencyHistogramName[] =
"Ash.Desks.AnimationLatency.DeskActivation";
constexpr char kDeskActivationSmoothnessHistogramName[] =
"Ash.Desks.AnimationSmoothness.DeskActivation";
constexpr char kDeskRemovalLatencyHistogramName[] =
"Ash.Desks.AnimationLatency.DeskRemoval";
constexpr char kDeskRemovalSmoothnessHistogramName[] =
"Ash.Desks.AnimationSmoothness.DeskRemoval";
// Measures the presentation time during a continuous gesture animation. This is
// the time from when we receive an Update request to the time the next frame is
// presented.
constexpr char kDeskUpdateGestureHistogramName[] =
"Ash.Desks.PresentationTime.UpdateGesture";
constexpr char kDeskUpdateGestureMaxLatencyHistogramName[] =
"Ash.Desks.PresentationTime.UpdateGesture.MaxLatency";
// The user ends a gesture swipe and triggers an animation to the closest desk.
// This histogram measures the smoothness of that animation.
constexpr char kDeskEndGestureSmoothnessHistogramName[] =
"Ash.Desks.AnimationSmoothness.DeskEndGesture";
// Swipes which are below this threshold are considered fast, and
// RootWindowDeskSwitchAnimator will determine a different ending desk for these
// swipes.
constexpr base::TimeDelta kFastSwipeThresholdDuration = base::Milliseconds(500);
bool IsForContinuousGestures(DesksSwitchSource source) {
return source == DesksSwitchSource::kDeskSwitchTouchpad;
}
} // namespace
// -----------------------------------------------------------------------------
// DeskActivationAnimation:
DeskActivationAnimation::DeskActivationAnimation(DesksController* controller,
int starting_desk_index,
int ending_desk_index,
DesksSwitchSource source,
bool update_window_activation)
: DeskAnimationBase(controller,
ending_desk_index,
IsForContinuousGestures(source)),
switch_source_(source),
update_window_activation_(update_window_activation),
visible_desk_index_(starting_desk_index),
last_start_or_replace_time_(base::TimeTicks::Now()),
presentation_time_recorder_(CreatePresentationTimeHistogramRecorder(
desks_util::GetSelectedCompositorForPerformanceMetrics(),
kDeskUpdateGestureHistogramName,
kDeskUpdateGestureMaxLatencyHistogramName)) {
DeskSwitchAnimationType type = DeskSwitchAnimationType::kQuickAnimation;
if (source == DesksSwitchSource::kDeskSwitchShortcut ||
source == DesksSwitchSource::kDeskSwitchTouchpad) {
type = DeskSwitchAnimationType::kContinuousAnimation;
}
for (aura::Window* root : Shell::GetAllRootWindows()) {
desk_switch_animators_.emplace_back(
std::make_unique<RootWindowDeskSwitchAnimator>(
root, type, starting_desk_index, ending_desk_index, this,
/*for_remove=*/false));
}
// On starting, the user may stay on the current desk for a touchpad swipe.
// All other switch sources are guaranteed to move at least once.
if (switch_source_ != DesksSwitchSource::kDeskSwitchTouchpad)
visible_desk_changes_ = 1;
}
DeskActivationAnimation::~DeskActivationAnimation() = default;
bool DeskActivationAnimation::Replace(bool moving_left,
DesksSwitchSource source) {
// Replacing an animation of a different switch source is not supported.
if (source != switch_source_)
return false;
// Do not log any EndSwipeAnimation smoothness metrics if the animation has
// been canceled midway by an Replace call.
if (is_continuous_gesture_animation_ && throughput_tracker_.has_value()) {
// Reset will call cancellation on tracker.
throughput_tracker_.reset();
}
// For fast swipes, we skip the implicit animation after ending screenshot in
// DeskAnimationBase, unless the swipe has ended and is deemed fast. Since
// Replace is called, the animation is refreshed by a new swipe and is no
// longer ending, so we rest this back to false.
did_continuous_gesture_end_fast_ = false;
// If any of the animators are still taking either screenshot, do not replace
// the animation.
for (const auto& animator : desk_switch_animators_) {
if (!animator->starting_desk_screenshot_taken() ||
!animator->ending_desk_screenshot_taken()) {
return false;
}
}
const int new_ending_desk_index = ending_desk_index_ + (moving_left ? -1 : 1);
// Already at the leftmost or rightmost desk, nothing to replace.
if (new_ending_desk_index < 0 ||
new_ending_desk_index >= static_cast<int>(controller_->desks().size())) {
return false;
}
ending_desk_index_ = new_ending_desk_index;
last_start_or_replace_time_ = base::TimeTicks::Now();
// Similar to on starting, for touchpad, the user can replace the animation
// without switching visible desks.
if (switch_source_ != DesksSwitchSource::kDeskSwitchTouchpad)
++visible_desk_changes_;
// List of animators that need a screenshot. It should be either empty or
// match the size of |desk_switch_animators_| as all the animations should be
// in sync.
std::vector<RootWindowDeskSwitchAnimator*> pending_animators;
for (const auto& animator : desk_switch_animators_) {
if (animator->ReplaceAnimation(new_ending_desk_index))
pending_animators.push_back(animator.get());
}
// No screenshot needed. Call OnEndingDeskScreenshotTaken which will start the
// animation.
if (pending_animators.empty()) {
OnEndingDeskScreenshotTaken();
return true;
}
// Activate the target desk and take a screenshot.
DCHECK_EQ(pending_animators.size(), desk_switch_animators_.size());
PrepareDeskForScreenshot(new_ending_desk_index);
for (auto* animator : pending_animators)
animator->TakeEndingDeskScreenshot();
return true;
}
bool DeskActivationAnimation::UpdateSwipeAnimation(float scroll_delta_x) {
if (!is_continuous_gesture_animation_)
return false;
presentation_time_recorder_->RequestNext();
auto* first_animator = desk_switch_animators_.front().get();
DCHECK(first_animator);
const bool old_reached_edge = first_animator->reached_edge();
// If any of the displays need a new screenshot while scrolling, take the
// ending desk screenshot for all of them to keep them in sync.
std::optional<int> ending_desk_index;
for (const auto& animator : desk_switch_animators_) {
if (!ending_desk_index)
ending_desk_index = animator->UpdateSwipeAnimation(scroll_delta_x);
else
animator->UpdateSwipeAnimation(scroll_delta_x);
}
// See if the animator of the first display has visibly changed desks. If so,
// update `visible_desk_changes_` for metrics collection purposes. Also fire a
// haptic event if we have reached the edge, or the visible desk has changed.
if (first_animator->starting_desk_screenshot_taken() &&
first_animator->ending_desk_screenshot_taken()) {
const int old_visible_desk_index = visible_desk_index_;
visible_desk_index_ = first_animator->GetIndexOfMostVisibleDeskScreenshot();
if (visible_desk_index_ != old_visible_desk_index) {
++visible_desk_changes_;
chromeos::haptics_util::PlayHapticTouchpadEffect(
ui::HapticTouchpadEffect::kTick,
ui::HapticTouchpadEffectStrength::kMedium);
}
const bool reached_edge = first_animator->reached_edge();
if (reached_edge && !old_reached_edge) {
chromeos::haptics_util::PlayHapticTouchpadEffect(
ui::HapticTouchpadEffect::kKnock,
ui::HapticTouchpadEffectStrength::kMedium);
}
}
// No screenshot needed.
if (!ending_desk_index)
return true;
// Activate the target desk and take a screenshot.
ending_desk_index_ = *ending_desk_index;
PrepareDeskForScreenshot(ending_desk_index_);
for (const auto& animator : desk_switch_animators_) {
animator->PrepareForEndingDeskScreenshot(ending_desk_index_);
animator->TakeEndingDeskScreenshot();
}
return true;
}
bool DeskActivationAnimation::EndSwipeAnimation() {
if (!is_continuous_gesture_animation_)
return false;
// Start tracking the animation smoothness after the continuous gesture swipe
// has ended.
throughput_tracker_ = desks_util::GetSelectedCompositorForPerformanceMetrics()
->RequestNewCompositorMetricsTracker();
throughput_tracker_->Start(
metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDeskEndGestureSmoothnessHistogramName,
smoothness);
})));
// End the animation. The animator will determine which desk to animate to,
// and update their ending desk index. When the animation is finished we will
// activate that desk. Set `did_continuous_gesture_end_fast_` to true if
// this is deemed a fast swipe. We will trigger the animation implicity if an
// ending screenshot is taken if so.
const bool is_fast_swipe =
base::TimeTicks::Now() - last_start_or_replace_time_ <
kFastSwipeThresholdDuration;
did_continuous_gesture_end_fast_ = is_fast_swipe;
// Ending the swipe animation on the animators may delete `this`. Use a local
// variable and weak pointer to validate and prevent use after free.
int ending_desk_index;
base::WeakPtr<DeskActivationAnimation> weak_ptr =
weak_ptr_factory_.GetWeakPtr();
for (const auto& animator : desk_switch_animators_) {
ending_desk_index = animator->EndSwipeAnimation(is_fast_swipe);
if (!weak_ptr)
return true;
}
ending_desk_index_ = ending_desk_index;
return true;
}
bool DeskActivationAnimation::CanEnterOverview() const {
return DeskAnimationBase::CanEnterOverview() &&
(switch_source_ == DesksSwitchSource::kDeskSwitchShortcut ||
switch_source_ == DesksSwitchSource::kDeskSwitchTouchpad ||
switch_source_ == DesksSwitchSource::kIndexedDeskSwitchShortcut);
}
void DeskActivationAnimation::OnStartingDeskScreenshotTakenInternal(
int ending_desk_index) {
DCHECK_EQ(ending_desk_index_, ending_desk_index);
PrepareDeskForScreenshot(ending_desk_index);
}
void DeskActivationAnimation::OnDeskSwitchAnimationFinishedInternal() {
// During a chained animation we may not switch desks if a replaced target
// desk does not require a new screenshot. If that is the case, activate the
// proper desk here.
ActivateDeskDuringAnimation(controller_->desks()[ending_desk_index_].get(),
update_window_activation_);
if (on_animation_finished_callback_for_testing_)
std::move(on_animation_finished_callback_for_testing_).Run();
}
DeskAnimationBase::LatencyReportCallback
DeskActivationAnimation::GetLatencyReportCallback() const {
return base::BindOnce([](const base::TimeDelta& latency) {
UMA_HISTOGRAM_TIMES(kDeskActivationLatencyHistogramName, latency);
});
}
metrics_util::ReportCallback
DeskActivationAnimation::GetSmoothnessReportCallback() const {
return metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDeskActivationSmoothnessHistogramName,
smoothness);
}));
}
void DeskActivationAnimation::AddOnAnimationFinishedCallbackForTesting(
base::OnceClosure callback) {
on_animation_finished_callback_for_testing_ = std::move(callback);
}
void DeskActivationAnimation::PrepareDeskForScreenshot(int index) {
HideActiveContextMenu();
// Check that ending_desk_index_ is in range.
// See crbug.com/1346900.
const auto& desks = controller_->desks();
CHECK_LT(static_cast<size_t>(ending_desk_index_), desks.size());
ActivateDeskDuringAnimation(desks[ending_desk_index_].get(),
update_window_activation_);
MaybeRestoreSplitView(/*refresh_snapped_windows=*/true);
}
// -----------------------------------------------------------------------------
// DeskRemovalAnimation:
DeskRemovalAnimation::DeskRemovalAnimation(DesksController* controller,
int desk_to_remove_index,
int desk_to_activate_index,
DesksCreationRemovalSource source,
DeskCloseType close_type)
: DeskAnimationBase(controller,
desk_to_activate_index,
/*is_continuous_gesture_animation=*/false),
desk_to_remove_index_(desk_to_remove_index),
request_source_(source),
close_type_(close_type) {
DCHECK(!Shell::Get()->overview_controller()->InOverviewSession());
DCHECK_EQ(controller_->active_desk(),
controller_->desks()[desk_to_remove_index_].get());
for (aura::Window* root : Shell::GetAllRootWindows()) {
auto animator = std::make_unique<RootWindowDeskSwitchAnimator>(
root, DeskSwitchAnimationType::kQuickAnimation, desk_to_remove_index_,
desk_to_activate_index, this,
/*for_remove=*/true);
animator->set_is_combine_desks_type(close_type ==
DeskCloseType::kCombineDesks);
desk_switch_animators_.emplace_back(std::move(animator));
}
}
DeskRemovalAnimation::~DeskRemovalAnimation() = default;
void DeskRemovalAnimation::OnStartingDeskScreenshotTakenInternal(
int ending_desk_index) {
DCHECK_EQ(ending_desk_index_, ending_desk_index);
DCHECK_EQ(controller_->active_desk(),
controller_->desks()[desk_to_remove_index_].get());
// We are removing the active desk, which may have tablet split view active.
// We will restore the split view state of the newly activated desk at the
// end of the animation. Clamshell split view is impossible because
// |DeskRemovalAnimation| is not used in overview.
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->EndSplitView(
SplitViewController::EndReason::kDesksChange);
HideActiveContextMenu();
// At the end of phase (1), we activate the target desk (i.e. the desk that
// will be activated after the active desk `desk_to_remove_index_` is
// removed). This means that phase (2) will take a screenshot of that desk
// before we move the windows of `desk_to_remove_index_` to that target desk.
ActivateDeskDuringAnimation(controller_->desks()[ending_desk_index_].get(),
/*update_window_activation=*/false);
}
void DeskRemovalAnimation::OnDeskSwitchAnimationFinishedInternal() {
// Do the actual desk removal behind the scenes before the screenshot layers
// are destroyed.
controller_->RemoveDeskInternal(
controller_->desks()[desk_to_remove_index_].get(), request_source_,
close_type_, /*desk_switched=*/true);
MaybeRestoreSplitView(/*refresh_snapped_windows=*/true);
}
DeskAnimationBase::LatencyReportCallback
DeskRemovalAnimation::GetLatencyReportCallback() const {
return base::BindOnce([](const base::TimeDelta& latency) {
UMA_HISTOGRAM_TIMES(kDeskRemovalLatencyHistogramName, latency);
});
}
metrics_util::ReportCallback DeskRemovalAnimation::GetSmoothnessReportCallback()
const {
return ash::metrics_util::ForSmoothnessV3(
base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDeskRemovalSmoothnessHistogramName,
smoothness);
}));
}
} // namespace ash