#include "ash/wm/window_restore/window_restore_controller.h"
#include <cstdint>
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/container_finder.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/templates/saved_desk_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_restore/window_restore_util.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/app_restore/full_restore_utils.h"
#include "components/app_restore/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/window_parenting_client.h"
#include "ui/aura/window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
WindowRestoreController* g_instance = nullptr;
WindowRestoreController::SaveWindowCallback g_save_window_callback_for_testing;
constexpr ShellWindowId kAppParentContainers[19] = {
kShellWindowId_DeskContainerA, kShellWindowId_DeskContainerB,
kShellWindowId_DeskContainerC, kShellWindowId_DeskContainerD,
kShellWindowId_DeskContainerE, kShellWindowId_DeskContainerF,
kShellWindowId_DeskContainerG, kShellWindowId_DeskContainerH,
kShellWindowId_DeskContainerI, kShellWindowId_DeskContainerJ,
kShellWindowId_DeskContainerK, kShellWindowId_DeskContainerL,
kShellWindowId_DeskContainerM, kShellWindowId_DeskContainerN,
kShellWindowId_DeskContainerO, kShellWindowId_DeskContainerP,
kShellWindowId_AlwaysOnTopContainer, kShellWindowId_FloatContainer,
kShellWindowId_UnparentedContainer,
};
constexpr chromeos::AppType kSupportedAppTypes[] = {
chromeos::AppType::BROWSER, chromeos::AppType::CHROME_APP,
chromeos::AppType::ARC_APP, chromeos::AppType::SYSTEM_APP};
constexpr base::TimeDelta kAllowActivationDelay = base::Seconds(2);
app_restore::WindowInfo* GetWindowInfo(aura::Window* window) {
return window->GetProperty(app_restore::kWindowInfoKey);
}
void MaybeRestoreOutOfBoundsWindows(aura::Window* window) {
app_restore::WindowInfo* window_info = GetWindowInfo(window);
if (!window_info)
return;
gfx::Rect current_bounds =
window_info->current_bounds.value_or(gfx::Rect(0, 0));
if (current_bounds.IsEmpty())
return;
const auto& closest_display =
display::Screen::Get()->GetDisplayNearestWindow(window);
const gfx::Rect display_area = closest_display.work_area();
if (display_area.Contains(current_bounds))
return;
AdjustBoundsToEnsureMinimumWindowVisibility(
display_area, false, ¤t_bounds);
auto* window_state = WindowState::Get(window);
if (window_state->HasRestoreBounds()) {
window_state->SetRestoreBoundsInScreen(current_bounds);
} else {
window->SetBoundsInScreen(current_bounds, closest_display);
}
}
class ParentChangeObserver : public aura::WindowObserver {
public:
ParentChangeObserver(aura::Window* window) {
DCHECK(!window->parent());
window_observation_.Observe(window);
}
ParentChangeObserver(const ParentChangeObserver&) = delete;
ParentChangeObserver& operator=(const ParentChangeObserver&) = delete;
~ParentChangeObserver() override = default;
void OnWindowParentChanged(aura::Window* window,
aura::Window* parent) override {
if (!parent)
return;
WindowRestoreController::Get()->SaveAllWindows();
delete this;
}
void OnWindowDestroying(aura::Window* window) override { delete this; }
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
};
}
WindowRestoreController::WindowRestoreController() {
DCHECK_EQ(nullptr, g_instance);
g_instance = this;
app_restore_info_observation_.Observe(
app_restore::AppRestoreInfo::GetInstance());
}
WindowRestoreController::~WindowRestoreController() {
DCHECK_EQ(this, g_instance);
g_instance = nullptr;
}
WindowRestoreController* WindowRestoreController::Get() {
return g_instance;
}
bool WindowRestoreController::CanActivateRestoredWindow(
const aura::Window* window) {
if (!window->GetProperty(app_restore::kLaunchedFromAppRestoreKey))
return true;
if (!desks_util::BelongsToActiveDesk(const_cast<aura::Window*>(window)))
return false;
const chromeos::AppType app_type = window->GetProperty(chromeos::kAppTypeKey);
const bool is_real_arc_window =
window->GetProperty(app_restore::kRealArcTaskWindow);
if (app_type == chromeos::AppType::ARC_APP && !is_real_arc_window) {
return true;
}
auto* desk_container = window->parent();
if (!desk_container || !desks_util::IsDeskContainer(desk_container))
return true;
auto siblings = desk_container->children();
for (aura::Window* const sibling : base::Reversed(siblings)) {
if (WindowState::Get(sibling)->IsMinimized())
continue;
return window == sibling;
}
return false;
}
bool WindowRestoreController::CanActivateAppList(const aura::Window* window) {
if (!display::Screen::Get()->InTabletMode()) {
return true;
}
auto* app_list_controller = Shell::Get()->app_list_controller();
if (!app_list_controller || app_list_controller->GetWindow() != window)
return true;
for (aura::Window* root_window : Shell::GetAllRootWindows()) {
auto active_desk_children =
desks_util::GetActiveDeskContainerForRoot(root_window)->children();
auto topmost_visible_iter = active_desk_children.rbegin();
while (topmost_visible_iter != active_desk_children.rend() &&
WindowState::Get(*topmost_visible_iter)->IsMinimized()) {
topmost_visible_iter = std::next(topmost_visible_iter);
}
if (topmost_visible_iter != active_desk_children.rend() &&
(*topmost_visible_iter)
->GetProperty(app_restore::kLaunchedFromAppRestoreKey)) {
return false;
}
}
return true;
}
std::vector<raw_ptr<aura::Window, VectorExperimental>>::const_iterator
WindowRestoreController::GetWindowToInsertBefore(
aura::Window* window,
const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows) {
const int32_t* activation_index =
window->GetProperty(app_restore::kActivationIndexKey);
DCHECK(activation_index);
if (saved_desk_util::IsWindowOnTopForTemplate(window)) {
for (auto it = windows.begin(); it != windows.end(); ++it) {
const int32_t* next_activation_index =
(*it)->GetProperty(app_restore::kActivationIndexKey);
if (next_activation_index && *activation_index > *next_activation_index) {
return it;
}
}
return windows.end();
}
auto it = windows.begin();
while (it != windows.end()) {
const int32_t* next_activation_index =
(*it)->GetProperty(app_restore::kActivationIndexKey);
if (!next_activation_index || *activation_index > *next_activation_index) {
return it;
}
it = std::next(it);
}
return it;
}
void WindowRestoreController::SaveWindow(WindowState* window_state) {
SaveWindowImpl(window_state, std::nullopt);
}
void WindowRestoreController::SaveAllWindows() {
auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kAllDesks);
for (int i = 0; i < static_cast<int>(mru_windows.size()); ++i) {
WindowState* window_state = WindowState::Get(mru_windows[i]);
SaveWindowImpl(window_state, i);
}
}
void WindowRestoreController::OnWindowActivated(aura::Window* gained_active) {
SaveAllWindows();
}
void WindowRestoreController::OnDisplayTabletStateChanged(
display::TabletState state) {
if (display::IsTabletStateChanging(state)) {
return;
}
SaveAllWindows();
}
void WindowRestoreController::OnRestorePrefChanged(const AccountId& account_id,
bool could_restore) {
if (could_restore)
SaveAllWindows();
}
void WindowRestoreController::OnAppLaunched(aura::Window* window) {
if (!IsArcWindow(window))
return;
if (window->parent()) {
SaveAllWindows();
return;
}
new ParentChangeObserver(window);
}
void WindowRestoreController::OnWidgetInitialized(views::Widget* widget) {
CHECK(widget);
aura::Window* window = widget->GetNativeWindow();
if (window->GetProperty(app_restore::kParentToHiddenContainerKey)) {
return;
}
if (!window->GetProperty(app_restore::kLaunchedFromAppRestoreKey)) {
return;
}
if (window->GetProperty(app_restore::kRestoreWindowIdKey) > -1) {
OverviewController::Get()->EndOverview(OverviewEndAction::kFullRestore);
}
UpdateAndObserveWindow(window);
MaybeRestoreOutOfBoundsWindows(window);
}
void WindowRestoreController::OnParentWindowToValidContainer(
aura::Window* window) {
DCHECK(window);
DCHECK(window->GetProperty(app_restore::kParentToHiddenContainerKey));
app_restore::WindowInfo* window_info = GetWindowInfo(window);
if (window_info) {
int desk_id = -1;
if (window_info->desk_guid.is_valid()) {
desk_id =
DesksController::Get()->GetDeskIndexByUuid(window_info->desk_guid);
}
if (desk_id == -1) {
desk_id = window_info->desk_id
? static_cast<int>(*window_info->desk_id)
: aura::client::kWindowWorkspaceUnassignedWorkspace;
}
window->SetProperty(aura::client::kWindowWorkspaceKey, desk_id);
}
window->SetProperty(app_restore::kParentToHiddenContainerKey, false);
aura::client::ParentWindowWithContext(window,
window->GetRootWindow(),
window->GetBoundsInScreen(),
display::kInvalidDisplayId);
UpdateAndObserveWindow(window);
}
void WindowRestoreController::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
if (key == app_restore::kRealArcTaskWindow &&
window->GetProperty(app_restore::kRealArcTaskWindow)) {
window->SetProperty(app_restore::kLaunchedFromAppRestoreKey, true);
restore_property_clear_callbacks_.emplace(
window, base::BindOnce(&WindowRestoreController::ClearLaunchedKey,
weak_ptr_factory_.GetWeakPtr(), window));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, restore_property_clear_callbacks_[window].callback(),
kAllowActivationDelay);
}
if (key != app_restore::kLaunchedFromAppRestoreKey ||
window->GetProperty(app_restore::kLaunchedFromAppRestoreKey)) {
return;
}
DCHECK(windows_observation_.IsObservingSource(window));
windows_observation_.RemoveObservation(window);
to_be_shown_windows_.erase(window);
if (base::Contains(restore_property_clear_callbacks_, window))
CancelAndRemoveRestorePropertyClearCallback(window);
}
void WindowRestoreController::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
if (!windows_observation_.IsObservingSource(window))
return;
if (!visible || !to_be_shown_windows_.contains(window))
return;
to_be_shown_windows_.erase(window);
RestoreStateTypeAndClearLaunchedKey(window);
aura::Window* app_list_window =
Shell::Get()->app_list_controller()->GetWindow();
if (!display::Screen::Get()->InTabletMode() || !app_list_window) {
return;
}
auto* app_list_widget =
views::Widget::GetWidgetForNativeWindow(app_list_window);
if (app_list_widget->IsActive() && WindowState::Get(window)->IsMaximized())
app_list_widget->Deactivate();
}
void WindowRestoreController::OnWindowDestroying(aura::Window* window) {
DCHECK(windows_observation_.IsObservingSource(window));
windows_observation_.RemoveObservation(window);
if (base::Contains(restore_property_clear_callbacks_, window))
ClearLaunchedKey(window);
}
void WindowRestoreController::UpdateAndObserveWindow(aura::Window* window) {
DCHECK(window);
DCHECK(window->parent());
windows_observation_.AddObservation(window);
if (WindowState::Get(window)->IsMinimized() || window->IsVisible()) {
RestoreStateTypeAndClearLaunchedKey(window);
} else {
to_be_shown_windows_.insert(window);
window->ClearProperty(aura::client::kRestoreShowStateKey);
}
StackWindow(window);
}
void WindowRestoreController::StackWindow(aura::Window* window) {
int32_t* activation_index =
window->GetProperty(app_restore::kActivationIndexKey);
if (!activation_index)
return;
Shell::Get()->mru_window_tracker()->OnWindowAlteredByWindowRestore(window);
auto siblings = window->parent()->children();
auto insertion_point = GetWindowToInsertBefore(window, siblings);
if (insertion_point != siblings.end())
window->parent()->StackChildBelow(window, *insertion_point);
}
bool WindowRestoreController::IsRestoringWindow(aura::Window* window) const {
return windows_observation_.IsObservingSource(window);
}
void WindowRestoreController::SaveWindowImpl(
WindowState* window_state,
std::optional<int> activation_index) {
DCHECK(window_state);
aura::Window* window = window_state->window();
if (window_state->IsPip() && IsArcWindow(window))
return;
if (!window->parent() ||
!base::Contains(kAppParentContainers, window->parent()->GetId())) {
return;
}
if (!base::Contains(kSupportedAppTypes,
window->GetProperty(chromeos::kAppTypeKey))) {
return;
}
if (!app_restore::AppRestoreInfo::GetInstance()->CanPerformRestore(
Shell::Get()->session_controller()->GetActiveAccountId())) {
return;
}
aura::Window::Windows mru_windows;
if (!activation_index.has_value()) {
mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kAllDesks);
}
std::unique_ptr<app_restore::WindowInfo> window_info =
BuildWindowInfo(window, activation_index, mru_windows);
::full_restore::SaveWindowInfo(*window_info);
if (g_save_window_callback_for_testing)
g_save_window_callback_for_testing.Run(*window_info);
}
void WindowRestoreController::RestoreStateTypeAndClearLaunchedKey(
aura::Window* window) {
app_restore::WindowInfo* window_info = GetWindowInfo(window);
if (window_info) {
auto state_type = window_info->window_state_type;
if (state_type) {
auto* window_state = WindowState::Get(window);
Shell::Get()->tablet_mode_controller()->AddWindow(window);
if (chromeos::IsSnappedWindowStateType(*state_type)) {
base::AutoReset<raw_ptr<aura::Window>> auto_reset_to_be_snapped(
&to_be_snapped_window_, window);
const float snap_ratio = window_info->snap_percentage.value_or(
chromeos::kDefaultSnapRatio * 100) *
0.01f;
const WindowSnapWMEvent snap_event(
*state_type == chromeos::WindowStateType::kPrimarySnapped
? WM_EVENT_SNAP_PRIMARY
: WM_EVENT_SNAP_SECONDARY,
snap_ratio,
WindowSnapActionSource::
kSnapByFullRestoreOrDeskTemplateOrSavedDesk);
window_state->OnWMEvent(&snap_event);
}
if (*state_type == chromeos::WindowStateType::kFloated) {
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state->OnWMEvent(&float_event);
}
}
}
restore_property_clear_callbacks_.emplace(
window, base::BindOnce(&WindowRestoreController::ClearLaunchedKey,
weak_ptr_factory_.GetWeakPtr(), window));
const chromeos::AppType app_type = window->GetProperty(chromeos::kAppTypeKey);
const bool is_real_arc_window =
window->GetProperty(app_restore::kRealArcTaskWindow);
const base::TimeDelta delay =
app_type == chromeos::AppType::CHROME_APP ||
(app_type == chromeos::AppType::ARC_APP && is_real_arc_window)
? kAllowActivationDelay
: base::TimeDelta();
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, restore_property_clear_callbacks_[window].callback(), delay);
}
void WindowRestoreController::ClearLaunchedKey(aura::Window* window) {
CancelAndRemoveRestorePropertyClearCallback(window);
if (!window->is_destroying())
window->SetProperty(app_restore::kLaunchedFromAppRestoreKey, false);
}
void WindowRestoreController::CancelAndRemoveRestorePropertyClearCallback(
aura::Window* window) {
DCHECK(window);
DCHECK(base::Contains(restore_property_clear_callbacks_, window));
restore_property_clear_callbacks_[window].Cancel();
restore_property_clear_callbacks_.erase(window);
}
void WindowRestoreController::SetSaveWindowCallbackForTesting(
SaveWindowCallback callback) {
g_save_window_callback_for_testing = std::move(callback);
}
}