chromium/ash/multi_user/user_switch_animator.cc

// 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