chromium/ash/wm/tablet_mode/tablet_mode_window_manager.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/wm/tablet_mode/tablet_mode_window_manager.h"

#include <memory>
#include <vector>

#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/tablet_mode/scoped_skip_user_session_blocked_check.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_toggle_fullscreen_event_handler.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/backdrop_controller.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "ash/wm/workspace_controller.h"
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/screen.h"
#include "ui/wm/core/scoped_animation_disabler.h"

namespace ash {

namespace {

using ::chromeos::WindowStateType;

// This function is called to check if window[i] is eligible to be carried over
// to split view mode during clamshell <-> tablet mode transition or multi-user
// switch transition. Returns true if windows[i] exists, is on |root_window|,
// and can snap in split view on |root_window|.
bool IsCarryOverCandidateForSplitView(
    const MruWindowTracker::WindowList& windows,
    size_t i,
    aura::Window* root_window) {
  return windows.size() > i && windows[i]->GetRootWindow() == root_window &&
         SplitViewController::Get(root_window)
             ->CanKeepCurrentSnapRatio(windows[i]);
}

// When switching to clamshell mode if all the following
// conditions are met:
// 1. `InClamshellSplitViewMode()` returns true;
// 2. Overview is either not active or empty;
// 3. Two windows are not in a snap group.
// This state will be out of the scope of the `SplitViewController` in clamshell
// mode and we should end split view and end overview if any. For more details,
// please refer to `split_view_controller.h`.
void MaybeEndSplitViewAndOverview() {
  Shell* shell = Shell::Get();
  OverviewController* overview_controller = shell->overview_controller();
  const bool empty_or_inactive_overview =
      !overview_controller->InOverviewSession() ||
      overview_controller->overview_session()->IsEmpty();
  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  auto* primary_window = split_view_controller->primary_window();
  auto* secondary_window = split_view_controller->secondary_window();
  const bool windows_in_snap_group =
      snap_group_controller && primary_window && secondary_window &&
      snap_group_controller->AreWindowsInSnapGroup(primary_window,
                                                   secondary_window);

  if (split_view_controller->InClamshellSplitViewMode() &&
      empty_or_inactive_overview && !windows_in_snap_group) {
    split_view_controller->EndSplitView(
        SplitViewController::EndReason::kExitTabletMode);
    overview_controller->EndOverview(OverviewEndAction::kSplitView);
  }
}

// Snap the carry over windows into splitview mode at |divider_position|.
// TODO(b/327269057): Refactor split view transition. Also determine whether we
// should snap the windows in mru order, since it can cause
// `SplitViewDivider::observed_windows()` to get out of order.
void DoSplitViewTransition(
    std::vector<std::pair<aura::Window*, WindowStateType>> windows,
    int divider_position,
    WindowSnapActionSource snap_action_source) {
  if (windows.empty())
    return;

  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  for (auto& iter : windows) {
    // Preserve the current snap ratio before transition, since
    // `SplitViewController::SnapWindow()` will send a new snap event with
    // `snap_ratio`.
    std::optional<float> snap_ratio =
        WindowState::Get(iter.first)->snap_ratio();
    split_view_controller->SnapWindow(
        /*window=*/iter.first,
        /*snap_position=*/iter.second == WindowStateType::kPrimarySnapped
            ? SnapPosition::kPrimary
            : SnapPosition::kSecondary,
        snap_action_source,
        /*activate_window=*/false,
        /*snap_ratio=*/snap_ratio ? *snap_ratio : chromeos::kDefaultSnapRatio);
  }

  // For clamshell split view mode, end splitview mode if we're in single
  // split mode or both snapped mode (in both cases overview is not active)
  // except for the case when two windows are in a snap group.
  // TODO(xdai): Refactoring SplitViewController to make SplitViewController to
  // handle this case.
  MaybeEndSplitViewAndOverview();
}

void UpdateDeskContainersBackdrops() {
  for (aura::Window* root : Shell::GetAllRootWindows()) {
    for (auto* desk_container : desks_util::GetDesksContainers(root)) {
      WorkspaceController* controller = GetWorkspaceController(desk_container);
      WorkspaceLayoutManager* layout_manager = controller->layout_manager();
      BackdropController* backdrop_controller =
          layout_manager->backdrop_controller();
      backdrop_controller->UpdateBackdrop();
    }
  }
}

}  // namespace

// Class which tells tablet mode controller to observe a given window for UMA
// logging purposes. Created before the window animations start. When this goes
// out of scope and the given window is not actually animating, tells tablet
// mode controller to stop observing.
class ScopedObserveWindowAnimation {
 public:
  ScopedObserveWindowAnimation(aura::Window* window,
                               TabletModeWindowManager* manager,
                               bool exiting_tablet_mode)
      : window_(window),
        manager_(manager),
        exiting_tablet_mode_(exiting_tablet_mode) {
    if (Shell::Get()->tablet_mode_controller() && window_) {
      Shell::Get()->tablet_mode_controller()->MaybeObserveBoundsAnimation(
          window_);
    }
  }

  ScopedObserveWindowAnimation(const ScopedObserveWindowAnimation&) = delete;
  ScopedObserveWindowAnimation& operator=(const ScopedObserveWindowAnimation&) =
      delete;

  ~ScopedObserveWindowAnimation() {
    // May be null on shutdown.
    if (!Shell::Get()->tablet_mode_controller())
      return;

    if (!window_)
      return;

    const bool is_animating =
        window_->layer()->GetAnimator()->IsAnimatingProperty(
            TabletModeController::GetObservedTabletTransitionProperty());
    // Stops observing if |window_| is not animating the property we care about,
    // or if it is not tracked by TabletModeWindowManager. When this object is
    // destroyed while exiting tablet mode, |window_| is no longer tracked, so
    // skip that check.
    if (is_animating &&
        (exiting_tablet_mode_ || manager_->IsTrackingWindow(window_))) {
      return;
    }

    Shell::Get()->tablet_mode_controller()->StopObservingAnimation(
        /*record_stats=*/false, /*delete_screenshot=*/true);
  }

 private:
  raw_ptr<aura::Window> window_;
  raw_ptr<TabletModeWindowManager> manager_;
  bool exiting_tablet_mode_;
};

TabletModeWindowManager::TabletModeWindowManager() = default;

TabletModeWindowManager::~TabletModeWindowManager() = default;

void TabletModeWindowManager::Init() {
  {
    ScopedObserveWindowAnimation scoped_observe(
        window_util::GetTopNonFloatedWindow(), this,
        /*exiting_tablet_mode=*/false);
    ArrangeWindowsForTabletMode();
  }
  AddWindowCreationObservers();
  display_observer_.emplace(this);
  SplitViewController::Get(Shell::GetPrimaryRootWindow())->AddObserver(this);
  Shell::Get()->session_controller()->AddObserver(this);
  Shell::Get()->overview_controller()->AddObserver(this);
  accounts_since_entering_tablet_.insert(
      Shell::Get()->session_controller()->GetActiveAccountId());
  event_handler_ = std::make_unique<TabletModeToggleFullscreenEventHandler>();
  tablet_mode_multitask_menu_controller_ =
      std::make_unique<TabletModeMultitaskMenuController>();
}

void TabletModeWindowManager::Shutdown(ShutdownReason shutdown_reason) {
  WindowAndStateTypeList carryover_windows_in_splitview;
  const bool was_in_overview =
      Shell::Get()->overview_controller()->InOverviewSession();

  if (shutdown_reason == ShutdownReason::kExitTabletUIMode) {
    // There are 4 cases when exiting tablet mode:
    // 1) overview is active but split view is inactive: keep overview active in
    //    clamshell mode.
    // 2) overview and splitview are both active: keep overview and splitview
    //    both active in clamshell mode, unless if it's single split state,
    //    splitview and overview will both be ended.
    // 3) overview is inactive but split view is active (two snapped windows):
    //    split view is no longer active. But the two snapped windows will still
    //    keep snapped in clamshell mode.
    // 4) overview and splitview are both inactive: keep the current behavior,
    //    i.e., restore all windows to its window state before entering tablet
    //    mode.

    // TODO(xdai): Instead of caching snapped windows and their state here, we
    // should try to see if it can be done in the WindowState::State impl.
    carryover_windows_in_splitview =
        GetCarryOverWindowsInSplitView(/*clamshell_to_tablet=*/false);

    // For case 2 and 3: End splitview mode for two snapped windows case or
    // single split case to match the clamshell split view behavior except for
    // the case when two windows are in a snap group. (there is no both snapped
    // state or single split state in clamshell split view). The windows will
    // still be kept snapped though.
    MaybeEndSplitViewAndOverview();
  }

  for (aura::Window* window : windows_to_track_)
    window->RemoveObserver(this);
  windows_to_track_.clear();
  SplitViewController::Get(Shell::GetPrimaryRootWindow())->RemoveObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
  Shell::Get()->overview_controller()->RemoveObserver(this);
  display_observer_.reset();
  RemoveWindowCreationObservers();

  if (shutdown_reason == ShutdownReason::kExitTabletUIMode) {
    ScopedObserveWindowAnimation scoped_observe(
        window_util::GetTopNonFloatedWindow(), this,
        /*exiting_tablet_mode=*/true);
    ArrangeWindowsForClamshellMode(carryover_windows_in_splitview,
                                   was_in_overview);
  } else {
    CHECK_EQ(shutdown_reason, ShutdownReason::kSystemShutdown);
    while (window_state_map_.size()) {
      WindowToState::iterator iter = window_state_map_.begin();
      iter->first->RemoveObserver(this);
      window_state_map_.erase(iter);
    }
  }
}

bool TabletModeWindowManager::IsTrackingWindow(aura::Window* window) {
  return base::Contains(window_state_map_, window);
}

int TabletModeWindowManager::GetNumberOfManagedWindows() {
  return window_state_map_.size();
}

void TabletModeWindowManager::AddWindow(aura::Window* window) {
  // Only add the window if it is a direct dependent of a container window
  // and not yet tracked.
  if (IsTrackingWindow(window) || !IsContainerWindow(window->parent()))
    return;

  TrackWindow(window);
}

void TabletModeWindowManager::WindowStateDestroyed(aura::Window* window) {
  // We come here because the tablet window state object was destroyed. It was
  // destroyed either because ForgetWindow() was called, or because its
  // associated window was destroyed. In both cases, the window must has removed
  // TabletModeWindowManager as an observer.
  DCHECK(!window->HasObserver(this));

  // The window state object might have been removed in OnWindowDestroying().
  auto it = window_state_map_.find(window);
  if (it != window_state_map_.end())
    window_state_map_.erase(it);
}

void TabletModeWindowManager::SetIgnoreWmEventsForExit() {
  is_exiting_ = true;
  for (auto& pair : window_state_map_)
    pair.second->set_ignore_wm_events(true);
}

void TabletModeWindowManager::StopWindowAnimations() {
  for (auto& pair : window_state_map_)
    pair.first->layer()->GetAnimator()->StopAnimating();
}

void TabletModeWindowManager::OnOverviewModeEndingAnimationComplete(
    bool canceled) {
  if (canceled)
    return;

  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());

  // Maximize all snapped windows upon exiting overview mode except snapped
  // windows in splitview mode. Note the snapped window might not be tracked in
  // our |window_state_map_|.
  // Leave snapped windows on inactive desks unchanged.
  const MruWindowTracker::WindowList windows =
      Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
          kActiveDesk);
  for (aura::Window* window : windows) {
    if (split_view_controller->primary_window() != window &&
        split_view_controller->secondary_window() != window) {
      MaximizeIfSnapped(window);
    }
  }
}

void TabletModeWindowManager::OnSplitViewStateChanged(
    SplitViewController::State previous_state,
    SplitViewController::State state) {
  // All TabletModeWindowState will ignore further WMEvents, but we still have
  // to manually prevent sending maximizing events to ClientControlledState ARC
  // windows e.g. ARC apps.
  if (is_exiting_)
    return;

  if (state != SplitViewController::State::kNoSnap)
    return;

  aura::Window* primary_root = Shell::GetPrimaryRootWindow();
  switch (SplitViewController::Get(primary_root)->end_reason()) {
    case SplitViewController::EndReason::kNormal:
    case SplitViewController::EndReason::kUnsnappableWindowActivated:
    case SplitViewController::EndReason::kRootWindowDestroyed:
      break;
    case SplitViewController::EndReason::kHomeLauncherPressed:
    case SplitViewController::EndReason::kActiveUserChanged:
    case SplitViewController::EndReason::kWindowDragStarted:
    case SplitViewController::EndReason::kExitTabletMode:
    case SplitViewController::EndReason::kDesksChange:
    case SplitViewController::EndReason::kSnapGroups:
      // For the case of kHomeLauncherPressed, the home launcher will minimize
      // the snapped windows after ending splitview, so avoid maximizing them
      // here. For the case of kActiveUserChanged, the snapped windows will be
      // used to restore the splitview layout when switching back, and it is
      // already too late to maximize them anyway (the for loop below would
      // iterate over windows in the newly activated user session).
      return;
  }

  // Maximize all snapped windows upon exiting split view mode. Note the snapped
  // window might not be tracked in our |window_state_map_|.
  // Leave snapped windows on inactive desks unchanged.
  const MruWindowTracker::WindowList windows =
      Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
          kActiveDesk);
  for (aura::Window* window : windows) {
    // Please notice, if there're multi displays in tablet mode, we should just
    // maximize snapped `window` which belongs to the primary root window.
    // Maximizing snapped `window` on the second display can trigger
    // `EndSplitView` which can trigger activating the overview focus widget,
    // but the pending activable window could be the window on the primary
    // display.
    if (window->GetRootWindow() != primary_root)
      continue;
    MaximizeIfSnapped(window);
  }
}

void TabletModeWindowManager::OnWindowDestroying(aura::Window* window) {
  if (IsContainerWindow(window)) {
    // container window can be removed on display destruction.
    window->RemoveObserver(this);
    observed_container_windows_.erase(window);
  } else if (base::Contains(windows_to_track_, window)) {
    // Added window was destroyed before being shown.
    windows_to_track_.erase(window);
    window->RemoveObserver(this);
  } else {
    // If a known window gets destroyed we need to remove all knowledge about
    // it.
    ForgetWindow(window, /*destroyed=*/true);
  }
}

void TabletModeWindowManager::OnWindowHierarchyChanged(
    const HierarchyChangeParams& params) {
  // A window can get removed and then re-added by a drag and drop operation.
  if (params.new_parent && IsContainerWindow(params.new_parent) &&
      !IsTrackingWindow(params.target)) {
    // Don't register the window if the window is invisible. Instead,
    // wait until it becomes visible because the client may update the
    // flag to control if the window should be added.
    if (!params.target->IsVisible()) {
      if (!base::Contains(windows_to_track_, params.target)) {
        windows_to_track_.insert(params.target);
        params.target->AddObserver(this);
      }
      return;
    }
    TrackWindow(params.target);
    // When the state got added, the "WM_EVENT_ADDED_TO_WORKSPACE" event got
    // already sent and we have to notify our state again.
    if (IsTrackingWindow(params.target)) {
      WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
      WindowState::Get(params.target)->OnWMEvent(&event);
    }
  }
}

void TabletModeWindowManager::OnWindowPropertyChanged(aura::Window* window,
                                                      const void* key,
                                                      intptr_t old) {
  // Stop managing |window| if it is moved to have a non-normal z-order.
  if (key == aura::client::kZOrderingKey &&
      window->GetProperty(aura::client::kZOrderingKey) !=
          ui::ZOrderLevel::kNormal) {
    ForgetWindow(window, false /* destroyed */);
  }
}

void TabletModeWindowManager::OnWindowBoundsChanged(
    aura::Window* window,
    const gfx::Rect& old_bounds,
    const gfx::Rect& new_bounds,
    ui::PropertyChangeReason reason) {
  if (!IsContainerWindow(window))
    return;

  auto* session = Shell::Get()->overview_controller()->overview_session();
  if (session)
    session->SuspendReposition();

  // Reposition all non maximizeable windows.
  for (auto& pair : window_state_map_) {
    TabletModeWindowState::UpdateWindowPosition(
        WindowState::Get(pair.first),
        WindowState::BoundsChangeAnimationType::kNone);
  }
  if (session)
    session->ResumeReposition();
}

void TabletModeWindowManager::OnWindowVisibilityChanged(aura::Window* window,
                                                        bool visible) {
  // Skip if it's already managed.
  if (IsTrackingWindow(window))
    return;

  if (IsContainerWindow(window->parent()) &&
      base::Contains(windows_to_track_, window) && visible) {
    TrackWindow(window);
    // When the state got added, the "WM_EVENT_ADDED_TO_WORKSPACE" event got
    // already sent and we have to notify our state again.
    if (IsTrackingWindow(window)) {
      WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
      WindowState::Get(window)->OnWMEvent(&event);
    }
  }
}

void TabletModeWindowManager::OnDisplayAdded(const display::Display& display) {
  DisplayConfigurationChanged();
}

void TabletModeWindowManager::OnDisplaysRemoved(
    const display::Displays& removed_displays) {
  DisplayConfigurationChanged();
}

void TabletModeWindowManager::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());

  // There is only one SplitViewController object for all user sessions, but
  // functionally, each user session independently can be in split view or not.
  // Here, a new user session has just been switched to, and if split view mode
  // is active then it was for the previous user session.
  // SplitViewController::EndSplitView() will perform some cleanup, including
  // setting |SplitViewController::left_window_| and
  // |SplitViewController::right_window_| to null, but the aura::Window objects
  // will be left unchanged to facilitate switching back.
  split_view_controller->EndSplitView(
      SplitViewController::EndReason::kActiveUserChanged);

  // If a user session is now active for the first time since clamshell mode,
  // then do the logic for carrying over snapped windows. Else recreate the
  // split view layout from the last time the current user session was active.
  bool refresh_snapped_windows = false;
  if (accounts_since_entering_tablet_.count(account_id) == 0u) {
    WindowAndStateTypeList windows_in_splitview =
        GetCarryOverWindowsInSplitView(/*clamshell_to_tablet=*/true);
    const int divider_position = CalculateCarryOverDividerPosition(
        windows_in_splitview, /*clamshell_to_tablet=*/true);
    DoSplitViewTransition(windows_in_splitview, divider_position,
                          WindowSnapActionSource::kSnapByDeskOrSessionChange);
    accounts_since_entering_tablet_.insert(account_id);
  } else {
    refresh_snapped_windows = true;
  }

  MaybeRestoreSplitView(refresh_snapped_windows);
}

gfx::Rect TabletModeWindowManager::GetWindowBoundsInScreen(
    aura::Window* window,
    bool from_clamshell) const {
  auto iter = window_state_map_.find(window);
  return !from_clamshell || iter == window_state_map_.end()
             ? window->GetBoundsInScreen()
             : iter->second->old_window_bounds_in_screen();
}

WindowStateType TabletModeWindowManager::GetWindowStateType(
    aura::Window* window,
    bool from_clamshell) const {
  auto iter = window_state_map_.find(window);
  return !from_clamshell || iter == window_state_map_.end()
             ? WindowState::Get(window)->GetStateType()
             : iter->second->old_state()->GetType();
}

TabletModeWindowManager::WindowAndStateTypeList
TabletModeWindowManager::GetCarryOverWindowsInSplitView(
    bool clamshell_to_tablet) const {
  // Use vector here to get eligible windows, so that the most recently used
  // window gets carried over to the split view first to prevent overview from
  // interfering with the window activation order.
  WindowAndStateTypeList windows;

  // Check the states of the topmost two non-overview windows to see if they are
  // eligible to be carried over to splitscreen. A window must meet
  // IsCarryOverCandidateForSplitView() to be carried over to splitscreen.
  MruWindowTracker::WindowList mru_windows =
      Shell::Get()->mru_window_tracker()->BuildWindowForCycleList(kActiveDesk);
  std::erase_if(mru_windows, [](aura::Window* window) {
    return window->GetProperty(chromeos::kIsShowingInOverviewKey);
  });
  aura::Window* root_window = Shell::GetPrimaryRootWindow();
  if (IsCarryOverCandidateForSplitView(mru_windows, 0u, root_window)) {
    if (GetWindowStateType(mru_windows[0], clamshell_to_tablet) ==
        WindowStateType::kPrimarySnapped) {
      windows.emplace_back(
          std::make_pair(mru_windows[0], WindowStateType::kPrimarySnapped));
      if (IsCarryOverCandidateForSplitView(mru_windows, 1u, root_window) &&
          GetWindowStateType(mru_windows[1], clamshell_to_tablet) ==
              WindowStateType::kSecondarySnapped) {
        windows.emplace_back(
            std::make_pair(mru_windows[1], WindowStateType::kSecondarySnapped));
      }
    } else if (GetWindowStateType(mru_windows[0], clamshell_to_tablet) ==
               WindowStateType::kSecondarySnapped) {
      windows.emplace_back(
          std::make_pair(mru_windows[0], WindowStateType::kSecondarySnapped));
      if (IsCarryOverCandidateForSplitView(mru_windows, 1u, root_window) &&
          GetWindowStateType(mru_windows[1], clamshell_to_tablet) ==
              WindowStateType::kPrimarySnapped) {
        windows.emplace_back(
            std::make_pair(mru_windows[1], WindowStateType::kPrimarySnapped));
      }
    }
  }
  return windows;
}

int TabletModeWindowManager::CalculateCarryOverDividerPosition(
    const WindowAndStateTypeList& windows_in_splitview,
    bool clamshell_to_tablet) const {
  aura::Window* left_window = nullptr;
  aura::Window* right_window = nullptr;
  for (auto& iter : windows_in_splitview) {
    if (iter.second == WindowStateType::kPrimarySnapped)
      left_window = iter.first;
    else if (iter.second == WindowStateType::kSecondarySnapped)
      right_window = iter.first;
  }
  if (!left_window && !right_window)
    return -1;

  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(
          left_window ? left_window : right_window);
  gfx::Rect work_area = display.work_area();
  gfx::Rect left_window_bounds =
      left_window ? GetWindowBoundsInScreen(left_window, clamshell_to_tablet)
                  : gfx::Rect();
  gfx::Rect right_window_bounds =
      right_window ? GetWindowBoundsInScreen(right_window, clamshell_to_tablet)
                   : gfx::Rect();

  const bool horizontal = IsLayoutHorizontal(display);
  const bool primary = IsLayoutPrimary(display);

  // We need to expand (or shrink) the width of the snapped windows by the half
  // of the divider width when to-clamshell (or to-tablet) transition happens
  // accordingly, because in tablet mode the "center" of the split view should
  // be the center of the divider.
  const int divider_padding =
      (clamshell_to_tablet ? -1 : 1) * kSplitviewDividerShortSideLength / 2;
  if (horizontal) {
    if (primary) {
      return left_window ? left_window_bounds.width() + divider_padding
                         : work_area.width() - right_window_bounds.width() -
                               divider_padding;
    } else {
      return left_window ? work_area.width() - left_window_bounds.width() -
                               divider_padding
                         : right_window_bounds.width() + divider_padding;
    }
  } else {
    if (primary) {
      return left_window ? left_window_bounds.height() + divider_padding
                         : work_area.height() - right_window_bounds.height() -
                               divider_padding;
    } else {
      return left_window ? work_area.height() - left_window_bounds.height() -
                               divider_padding
                         : right_window_bounds.height() + divider_padding;
    }
  }
}

void TabletModeWindowManager::ArrangeWindowsForTabletMode() {
  // There are 3 cases when entering tablet mode:
  // 1) overview is active but split view is inactive: keep overview active in
  //    tablet mode.
  // 2) overview and splitview are both active (splitview can only be active
  //    when overview is active in clamshell mode): keep overview and splitview
  //    both active in tablet mode.
  // 3) overview is inactive: keep the current behavior, i.e.,
  //    a. if the top window is a snapped window, put it in splitview
  //    b. if the second top window is also a snapped window and snapped to
  //       the other side, put it in split view as well. Otherwise, open
  //       overview on the other side of the screen
  //    c. if the top window is not a snapped window, maximize all windows
  //       when entering tablet mode.

  // |activatable_windows| includes all windows to be tracked, and that includes
  // windows on the lock screen via |scoped_skip_user_session_blocked_check|.
  ScopedSkipUserSessionBlockedCheck scoped_skip_user_session_blocked_check;
  MruWindowTracker::WindowList activatable_windows =
      Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(kAllDesks);

  // Determine which windows are to be carried over to splitview from clamshell
  // mode to tablet mode.
  WindowAndStateTypeList windows_in_splitview =
      GetCarryOverWindowsInSplitView(/*clamshell_to_tablet=*/true);
  const int divider_position = CalculateCarryOverDividerPosition(
      windows_in_splitview, /*clamshell_to_tablet=*/true);

  // If split view is not appropriate, then maximize all windows and bail out.
  if (windows_in_splitview.empty()) {
    for (aura::Window* window : activatable_windows) {
      TrackWindow(window, /*entering_tablet_mode=*/true);
    }
    return;
  }

  // Carry over the state types of the windows that shall be in split view.
  // Maximize all other windows. Do not animate any window bounds updates.
  for (aura::Window* window : activatable_windows) {
    bool snap = false;
    for (auto& iter : windows_in_splitview) {
      if (window == iter.first) {
        snap = true;
        break;
      }
    }
    TrackWindow(window, /*entering_tablet_mode=*/true, snap,
                /*animate_bounds_on_attach=*/false);
  }

  // Do split view mode transition.
  DoSplitViewTransition(
      windows_in_splitview, divider_position,
      WindowSnapActionSource::kSnapByClamshellTabletTransition);
}

void TabletModeWindowManager::ArrangeWindowsForClamshellMode(
    WindowAndStateTypeList windows_in_splitview,
    bool was_in_overview) {
  const int divider_position = CalculateCarryOverDividerPosition(
      windows_in_splitview, /*clamshell_to_tablet=*/false);

  while (window_state_map_.size()) {
    aura::Window* window = window_state_map_.begin()->first;
    ForgetWindow(window, /*destroyed=*/false, was_in_overview);
  }

  // Arriving here the window state has changed to its clamshell window state.
  // Since we need to keep the windows that were in splitview still be snapped
  // in clamshell mode, change its window state to the corresponding snapped
  // window state.
  DoSplitViewTransition(
      windows_in_splitview, divider_position,
      WindowSnapActionSource::kSnapByClamshellTabletTransition);
}

void TabletModeWindowManager::TrackWindow(aura::Window* window,
                                          bool entering_tablet_mode,
                                          bool snap,
                                          bool animate_bounds_on_attach) {
  // Now that we are tracking it (or finding out it cannot be tracked), remove
  // it from `windows_to_track_`.
  if (base::Contains(windows_to_track_, window)) {
    windows_to_track_.erase(window);
    window->RemoveObserver(this);
  }

  if (!ShouldHandleWindow(window))
    return;

  DCHECK(!IsTrackingWindow(window));
  window->AddObserver(this);

  // Create and remember a tablet mode state which will attach itself to the
  // provided state object.
  window_state_map_.emplace(
      window, new TabletModeWindowState(window, weak_ptr_factory_.GetWeakPtr(),
                                        snap, animate_bounds_on_attach,
                                        entering_tablet_mode));
}

void TabletModeWindowManager::ForgetWindow(aura::Window* window,
                                           bool destroyed,
                                           bool was_in_overview) {
  windows_to_track_.erase(window);
  window->RemoveObserver(this);

  WindowToState::iterator it = window_state_map_.find(window);
  // A window may not be registered yet if the observer was
  // registered in OnWindowHierarchyChanged.
  if (it == window_state_map_.end())
    return;

  if (destroyed) {
    // If the window is to-be-destroyed, remove it from |window_state_map_|
    // immidietely. Otherwise it's possible to send a WMEvent to the to-be-
    // destroyed window.  Note we should not restore its old previous window
    // state object here since it will send unnecessary window state change
    // events. The tablet window state object and the old window state object
    // will be both deleted when the window is destroyed.
    window_state_map_.erase(it);
  } else {
    // By telling the state object to revert, it will switch back the old
    // State object and destroy itself, calling WindowStateDestroyed().
    it->second->LeaveTabletMode(WindowState::Get(it->first), was_in_overview);
    DCHECK(!IsTrackingWindow(window));
  }
}

bool TabletModeWindowManager::ShouldHandleWindow(aura::Window* window) {
  DCHECK(window);

  // Windows that don't have normal z-ordering should be free-floating and thus
  // not managed by us.
  if (window->GetProperty(aura::client::kZOrderingKey) !=
      ui::ZOrderLevel::kNormal) {
    return false;
  }

  // If the changing bounds in the maximized/fullscreen is allowed, then
  // let the client manage it even in tablet mode.
  if (!WindowState::Get(window) ||
      WindowState::Get(window)->allow_set_bounds_direct()) {
    return false;
  }

  return window->GetType() == aura::client::WINDOW_TYPE_NORMAL;
}

void TabletModeWindowManager::AddWindowCreationObservers() {
  DCHECK(observed_container_windows_.empty());
  // Observe window activations/creations in the default containers on all root
  // windows.
  for (aura::Window* root : Shell::GetAllRootWindows()) {
    for (auto* desk_container : desks_util::GetDesksContainers(root)) {
      DCHECK(!base::Contains(observed_container_windows_, desk_container));
      desk_container->AddObserver(this);
      observed_container_windows_.insert(desk_container);
    }
  }
}

void TabletModeWindowManager::RemoveWindowCreationObservers() {
  for (aura::Window* window : observed_container_windows_)
    window->RemoveObserver(this);
  observed_container_windows_.clear();
}

void TabletModeWindowManager::DisplayConfigurationChanged() {
  RemoveWindowCreationObservers();
  AddWindowCreationObservers();
  UpdateDeskContainersBackdrops();
}

bool TabletModeWindowManager::IsContainerWindow(aura::Window* window) {
  return base::Contains(observed_container_windows_, window);
}

}  // namespace ash