// Copyright 2014 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/multi_user/user_switch_animator.h"
#include <memory>
#include "ash/multi_user/multi_user_window_manager_impl.h"
#include "ash/public/cpp/multi_user_window_manager_delegate.h"
#include "ash/shell.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/window_positioner.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// The minimal possible animation time for animations which should happen
// "instantly".
constexpr base::TimeDelta kMinimalAnimationTime = base::Milliseconds(1);
// logic while the user gets switched.
class UserChangeActionDisabler {
public:
UserChangeActionDisabler() {
window_positioner::DisableAutoPositioning(true);
Shell::Get()->mru_window_tracker()->SetIgnoreActivations(true);
}
UserChangeActionDisabler(const UserChangeActionDisabler&) = delete;
UserChangeActionDisabler& operator=(const UserChangeActionDisabler&) = delete;
~UserChangeActionDisabler() {
window_positioner::DisableAutoPositioning(false);
Shell::Get()->mru_window_tracker()->SetIgnoreActivations(false);
}
};
// Defines an animation watcher for the 'hide' animation of the first maximized
// window we encounter while looping through the old user's windows. This is
// to observe the end of the animation so that we can destruct the old detached
// layer of the window.
class MaximizedWindowAnimationWatcher : public ui::ImplicitAnimationObserver {
public:
explicit MaximizedWindowAnimationWatcher(
std::unique_ptr<ui::LayerTreeOwner> old_layer)
: old_layer_(std::move(old_layer)) {}
MaximizedWindowAnimationWatcher(const MaximizedWindowAnimationWatcher&) =
delete;
MaximizedWindowAnimationWatcher& operator=(
const MaximizedWindowAnimationWatcher&) = delete;
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override { delete this; }
private:
std::unique_ptr<ui::LayerTreeOwner> old_layer_;
};
// Modifies the given |window_list| such that the most-recently used window (if
// any, and if it exists in |window_list|) will be the last window in the list.
void PutMruWindowLast(
std::vector<raw_ptr<aura::Window, VectorExperimental>>* window_list) {
DCHECK(window_list);
auto it = base::ranges::find_if(*window_list, &wm::IsActiveWindow);
if (it == window_list->end())
return;
// Move the active window to the end of the list.
aura::Window* active_window = *it;
window_list->erase(it);
window_list->push_back(active_window);
}
} // namespace
UserSwitchAnimator::UserSwitchAnimator(MultiUserWindowManagerImpl* owner,
const AccountId& new_account_id,
base::TimeDelta animation_speed)
: owner_(owner),
new_account_id_(new_account_id),
animation_speed_(animation_speed),
animation_step_(ANIMATION_STEP_HIDE_OLD_USER),
screen_cover_(GetScreenCover(NULL)),
windows_by_account_id_() {
Shell::Get()->overview_controller()->EndOverview(
OverviewEndAction::kUserSwitch);
BuildUserToWindowsListMap();
AdvanceUserTransitionAnimation();
if (animation_speed_.is_zero()) {
FinalizeAnimation();
} else {
user_changed_animation_timer_ = std::make_unique<base::RepeatingTimer>();
user_changed_animation_timer_->Start(
FROM_HERE, animation_speed_,
base::BindRepeating(&UserSwitchAnimator::AdvanceUserTransitionAnimation,
base::Unretained(this)));
}
}
UserSwitchAnimator::~UserSwitchAnimator() {
FinalizeAnimation();
}
// static
bool UserSwitchAnimator::CoversScreen(aura::Window* window) {
// Full screen covers the screen naturally. Since a normal window can have the
// same size as the work area, we only compare the bounds against the work
// area.
if (wm::WindowStateIs(window, ui::SHOW_STATE_FULLSCREEN))
return true;
gfx::Rect bounds = window->GetBoundsInScreen();
gfx::Rect work_area =
display::Screen::GetScreen()->GetDisplayNearestWindow(window).work_area();
bounds.Intersect(work_area);
return work_area == bounds;
}
void UserSwitchAnimator::AdvanceUserTransitionAnimation() {
DCHECK_NE(animation_step_, ANIMATION_STEP_ENDED);
TransitionWallpaper(animation_step_);
TransitionUserShelf(animation_step_);
TransitionWindows(animation_step_);
// Advance to the next step.
switch (animation_step_) {
case ANIMATION_STEP_HIDE_OLD_USER:
animation_step_ = ANIMATION_STEP_SHOW_NEW_USER;
break;
case ANIMATION_STEP_SHOW_NEW_USER:
animation_step_ = ANIMATION_STEP_FINALIZE;
break;
case ANIMATION_STEP_FINALIZE:
user_changed_animation_timer_.reset();
animation_step_ = ANIMATION_STEP_ENDED;
break;
case ANIMATION_STEP_ENDED:
NOTREACHED();
}
}
void UserSwitchAnimator::CancelAnimation() {
animation_step_ = ANIMATION_STEP_ENDED;
}
void UserSwitchAnimator::FinalizeAnimation() {
user_changed_animation_timer_.reset();
while (ANIMATION_STEP_ENDED != animation_step_)
AdvanceUserTransitionAnimation();
}
void UserSwitchAnimator::TransitionWallpaper(AnimationStep animation_step) {
auto* wallpaper_controller = Shell::Get()->wallpaper_controller();
// Handle the wallpaper switch.
if (animation_step == ANIMATION_STEP_HIDE_OLD_USER) {
// Set the wallpaper cross dissolve animation duration to our complete
// animation cycle for a fade in and fade out.
base::TimeDelta duration =
animation_speed_ * (NO_USER_COVERS_SCREEN == screen_cover_ ? 2 : 0);
wallpaper_controller->SetAnimationDuration(
duration > kMinimalAnimationTime ? duration : kMinimalAnimationTime);
if (screen_cover_ != NEW_USER_COVERS_SCREEN) {
wallpaper_controller->ShowUserWallpaper(new_account_id_);
wallpaper_user_id_for_test_ =
(NO_USER_COVERS_SCREEN == screen_cover_ ? "->" : "") +
new_account_id_.Serialize();
}
} else if (animation_step == ANIMATION_STEP_FINALIZE) {
// Revert the wallpaper cross dissolve animation duration back to the
// default.
if (screen_cover_ == NEW_USER_COVERS_SCREEN)
wallpaper_controller->ShowUserWallpaper(new_account_id_);
// Coming here the wallpaper user id is the final result. No matter how we
// got here.
wallpaper_user_id_for_test_ = new_account_id_.Serialize();
wallpaper_controller->SetAnimationDuration(base::TimeDelta());
}
}
void UserSwitchAnimator::TransitionUserShelf(AnimationStep animation_step) {
if (animation_step != ANIMATION_STEP_SHOW_NEW_USER)
return;
owner_->delegate_->OnTransitionUserShelfToNewAccount();
}
void UserSwitchAnimator::TransitionWindows(AnimationStep animation_step) {
// Disable the window position manager and the MRU window tracker temporarily.
UserChangeActionDisabler disabler;
// Animation duration.
base::TimeDelta duration =
base::Milliseconds(std::max(kMinimalAnimationTime.InMilliseconds(),
2 * animation_speed_.InMilliseconds()));
switch (animation_step) {
case ANIMATION_STEP_HIDE_OLD_USER: {
// Hide the old users.
for (auto& user_pair : windows_by_account_id_) {
auto& show_for_account_id = user_pair.first;
if (show_for_account_id == new_account_id_) {
continue;
}
bool found_foreground_maximized_window = false;
// We hide the windows such that the MRU window is the last one to be
// hidden, at which point all other windows have already been hidden,
// and hence the FocusController will not be able to find a next
// activateable window to restore focus to, and so we don't change
// window order (crbug.com/424307).
PutMruWindowLast(&(user_pair.second));
for (aura::Window* window : user_pair.second) {
// Minimized visiting windows (minimized windows with an owner
// different than that of the for_show_account_id) should return to
// their
// original owners' desktops.
MultiUserWindowManagerImpl::WindowToEntryMap::const_iterator itr =
owner_->window_to_entry().find(window);
DCHECK(itr != owner_->window_to_entry().end());
if (show_for_account_id != itr->second->owner() &&
wm::WindowStateIs(window, ui::SHOW_STATE_MINIMIZED)) {
owner_->ShowWindowForUserIntern(window, itr->second->owner());
wm::Unminimize(window);
continue;
}
if (!found_foreground_maximized_window && CoversScreen(window) &&
screen_cover_ == BOTH_USERS_COVER_SCREEN) {
// Maximized windows should be hidden, but visually kept visible
// in order to prevent showing the background while the animation is
// in progress. Therefore we detach the old layer and recreate fresh
// ones. The old layers will be destructed at the animation step
// |ANIMATION_STEP_FINALIZE|.
// old_layers_.push_back(wm::RecreateLayers(window));
// We only want to do this for the first (foreground) maximized
// window we encounter.
found_foreground_maximized_window = true;
std::unique_ptr<ui::LayerTreeOwner> old_layer =
wm::RecreateLayers(window);
window->layer()->parent()->StackAtBottom(old_layer->root());
ui::ScopedLayerAnimationSettings settings(
window->layer()->GetAnimator());
settings.AddObserver(
new MaximizedWindowAnimationWatcher(std::move(old_layer)));
// Call SetWindowVisibility() within the scope of |settings| so that
// MaximizedWindowAnimationWatcher is notified when the animation
// completes.
owner_->SetWindowVisibility(window, false, duration);
} else {
owner_->SetWindowVisibility(window, false, duration);
}
}
}
// Show new user.
auto new_user_itr = windows_by_account_id_.find(new_account_id_);
auto* desks_controller = Shell::Get()->desks_controller();
if (new_user_itr == windows_by_account_id_.end()) {
// Despite no new windows being shown, we still need to call
// DesksController::OnNewUserShown() to properly restack visible on all
// desks windows.
desks_controller->OnNewUserShown();
return;
}
for (aura::Window* window : new_user_itr->second) {
auto entry = owner_->window_to_entry().find(window);
DCHECK(entry != owner_->window_to_entry().end());
if (entry->second->show()) {
owner_->SetWindowVisibility(window, true, duration);
}
}
desks_controller->OnNewUserShown();
break;
}
case ANIMATION_STEP_SHOW_NEW_USER: {
// In order to make the animation look better, we had to move the code
// that shows the new user to the previous step. Hence, we do nothing
// here.
break;
}
case ANIMATION_STEP_FINALIZE: {
// Reactivate the MRU window of the new user.
aura::Window::Windows mru_list =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
if (!mru_list.empty()) {
aura::Window* window = mru_list[0];
if (owner_->IsWindowOnDesktopOfUser(window, new_account_id_) &&
!wm::WindowStateIs(window, ui::SHOW_STATE_MINIMIZED)) {
// Several unit tests come here without an activation client.
wm::ActivationClient* client =
wm::GetActivationClient(window->GetRootWindow());
if (client) {
client->ActivateWindow(window);
}
}
}
break;
}
case ANIMATION_STEP_ENDED:
NOTREACHED_IN_MIGRATION();
break;
}
}
UserSwitchAnimator::TransitioningScreenCover UserSwitchAnimator::GetScreenCover(
aura::Window* root_window) {
TransitioningScreenCover cover = NO_USER_COVERS_SCREEN;
for (auto& pair : owner_->window_to_entry()) {
aura::Window* window = pair.first;
if (root_window && window->GetRootWindow() != root_window)
continue;
if (window->IsVisible() && CoversScreen(window)) {
if (cover == NEW_USER_COVERS_SCREEN)
return BOTH_USERS_COVER_SCREEN;
else
cover = OLD_USER_COVERS_SCREEN;
} else if (owner_->IsWindowOnDesktopOfUser(window, new_account_id_) &&
CoversScreen(window)) {
if (cover == OLD_USER_COVERS_SCREEN)
return BOTH_USERS_COVER_SCREEN;
else
cover = NEW_USER_COVERS_SCREEN;
}
}
return cover;
}
void UserSwitchAnimator::BuildUserToWindowsListMap() {
// This is to be called only at the time this animation is constructed.
DCHECK(windows_by_account_id_.empty());
// For each unique parent window, we enumerate its children windows, and
// for each child if it's in the |window_to_entry()| map, we add it to the
// |windows_by_account_id_| map.
// This gives us a list of windows per each user that is in the same order
// they were created in their parent windows.
std::set<aura::Window*> parent_windows;
auto& window_to_entry_map = owner_->window_to_entry();
for (auto& window_entry_pair : window_to_entry_map) {
aura::Window* parent_window = window_entry_pair.first->parent();
if (!base::Contains(parent_windows, parent_window)) {
parent_windows.insert(parent_window);
for (aura::Window* child_window : parent_window->children()) {
auto itr = window_to_entry_map.find(child_window);
if (itr != window_to_entry_map.end()) {
windows_by_account_id_[itr->second->show_for_user()].push_back(
child_window);
}
}
}
}
}
} // namespace ash