blob: c41cd723e4c43a3dadefb0ba15f16ef33c7c258a [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/wm/desks/desk_animation_impl.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/utility/haptics_util.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_histogram_enums.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/bind.h"
#include "base/metrics/histogram_macros.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)) {
for (auto* root : Shell::GetAllRootWindows()) {
desk_switch_animators_.emplace_back(
std::make_unique<RootWindowDeskSwitchAnimator>(
root, 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_.Cancel();
// 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.
absl::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_;
haptics_util::PlayHapticTouchpadEffect(
ui::HapticTouchpadEffect::kTick,
ui::HapticTouchpadEffectStrength::kMedium);
}
const bool reached_edge = first_animator->reached_edge();
if (reached_edge && !old_reached_edge) {
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_.Start(
metrics_util::ForSmoothness(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;
}
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.
controller_->ActivateDeskInternal(
controller_->desks()[ending_desk_index_].get(),
update_window_activation_);
}
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::ForSmoothness(base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDeskActivationSmoothnessHistogramName,
smoothness);
}));
}
void DeskActivationAnimation::PrepareDeskForScreenshot(int index) {
for (auto* root_window_controller : Shell::GetAllRootWindowControllers())
root_window_controller->HideContextMenuNoAnimation();
// The order here matters. Overview must end before ending tablet split view
// before switching desks. (If clamshell split view is active on one or more
// displays, then it simply will end when we end overview.) That's because
// we don't want |TabletModeWindowManager| maximizing all windows because we
// cleared the snapped ones in |SplitViewController| first. See
// |TabletModeWindowManager::OnOverviewModeEndingAnimationComplete|.
// See also test coverage for this case in
// `TabletModeDesksTest.SnappedStateRetainedOnSwitchingDesksFromOverview`.
const bool in_overview =
Shell::Get()->overview_controller()->InOverviewSession();
if (in_overview) {
// Exit overview mode immediately without any animations before taking the
// ending desk screenshot. This makes sure that the ending desk
// screenshot will only show the windows in that desk, not overview stuff.
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kDeskActivation,
OverviewEnterExitType::kImmediateExit);
}
SplitViewController* split_view_controller =
SplitViewController::Get(Shell::GetPrimaryRootWindow());
split_view_controller->EndSplitView(
SplitViewController::EndReason::kDesksChange);
controller_->ActivateDeskInternal(
controller_->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 (auto* root : Shell::GetAllRootWindows()) {
desk_switch_animators_.emplace_back(
std::make_unique<RootWindowDeskSwitchAnimator>(
root, desk_to_remove_index_, desk_to_activate_index, this,
/*for_remove=*/true));
}
}
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);
for (auto* root_window_controller : Shell::GetAllRootWindowControllers())
root_window_controller->HideContextMenuNoAnimation();
// 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.
controller_->ActivateDeskInternal(
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_);
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::ForSmoothness(
base::BindRepeating([](int smoothness) {
UMA_HISTOGRAM_PERCENTAGE(kDeskRemovalSmoothnessHistogramName,
smoothness);
}));
}
} // namespace ash