| // Copyright 2018 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 "chrome/browser/ui/ash/launcher/crostini_app_window_shelf_controller.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "ash/public/cpp/shelf_model.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/shell.h" |
| #include "base/bind.h" |
| #include "base/strings/string_util.h" |
| #include "chrome/browser/chromeos/crostini/crostini_registry_service.h" |
| #include "chrome/browser/chromeos/crostini/crostini_registry_service_factory.h" |
| #include "chrome/browser/chromeos/crostini/crostini_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/ash/launcher/app_window_base.h" |
| #include "chrome/browser/ui/ash/launcher/app_window_launcher_item_controller.h" |
| #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h" |
| #include "chrome/browser/ui/ash/launcher/shelf_spinner_controller.h" |
| #include "chrome/browser/ui/ash/multi_user/multi_user_window_manager.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "components/exo/shell_surface.h" |
| #include "components/user_manager/user_manager.h" |
| #include "extensions/browser/app_window/app_window.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/env.h" |
| #include "ui/base/base_window.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace { |
| |
| void MoveWindowFromOldDisplayToNewDisplay(aura::Window* window, |
| display::Display& old_display, |
| display::Display& new_display) { |
| // Adjust the window size and origin in proportion to the relative size of the |
| // display. |
| int old_width = old_display.bounds().width(); |
| int new_width = new_display.bounds().width(); |
| int old_height = old_display.bounds().height(); |
| int new_height = new_display.bounds().height(); |
| gfx::Rect old_bounds = window->bounds(); |
| gfx::Rect new_bounds(old_bounds.x() * new_width / old_width, |
| old_bounds.y() * new_height / old_height, |
| old_bounds.width() * new_width / old_width, |
| old_bounds.height() * new_height / old_height); |
| |
| // Transform the bounds in display to that in screen. |
| gfx::Point new_origin = new_display.bounds().origin(); |
| new_origin.Offset(new_bounds.x(), new_bounds.y()); |
| new_bounds.set_origin(new_origin); |
| window->SetBoundsInScreen(new_bounds, new_display); |
| } |
| |
| } // namespace |
| |
| CrostiniAppWindowShelfController::CrostiniAppWindowShelfController( |
| ChromeLauncherController* owner) |
| : AppWindowLauncherController(owner) { |
| // TODO(mash): Find another way to observe for crostini app window creation. |
| // https://crbug.com/887156 |
| if (!features::IsMultiProcessMash()) |
| ash::Shell::Get()->aura_env()->AddObserver(this); |
| } |
| |
| CrostiniAppWindowShelfController::~CrostiniAppWindowShelfController() { |
| for (auto* window : observed_windows_) |
| window->RemoveObserver(this); |
| if (!features::IsMultiProcessMash()) |
| ash::Shell::Get()->aura_env()->RemoveObserver(this); |
| } |
| |
| void CrostiniAppWindowShelfController::AddToShelf(aura::Window* window, |
| AppWindowBase* app_window) { |
| ash::ShelfID shelf_id = app_window->shelf_id(); |
| AppWindowLauncherItemController* item_controller = |
| owner()->shelf_model()->GetAppWindowLauncherItemController(shelf_id); |
| if (item_controller == nullptr) { |
| auto controller = |
| std::make_unique<AppWindowLauncherItemController>(shelf_id); |
| item_controller = controller.get(); |
| if (!owner()->GetItem(shelf_id)) { |
| owner()->CreateAppLauncherItem(std::move(controller), |
| ash::STATUS_RUNNING); |
| } else { |
| owner()->shelf_model()->SetShelfItemDelegate(shelf_id, |
| std::move(controller)); |
| owner()->SetItemStatus(shelf_id, ash::STATUS_RUNNING); |
| } |
| } |
| |
| window->SetProperty(ash::kShelfIDKey, new std::string(shelf_id.Serialize())); |
| item_controller->AddWindow(app_window); |
| app_window->SetController(item_controller); |
| } |
| |
| ash::ShelfID CrostiniAppWindowShelfController::RemoveFromShelf( |
| aura::Window* window, |
| AppWindowBase* app_window) { |
| UnregisterAppWindow(app_window); |
| |
| // Check if we may close controller now, at this point we can safely remove |
| // controllers without window. |
| AppWindowLauncherItemController* item_controller = |
| owner()->shelf_model()->GetAppWindowLauncherItemController( |
| app_window->shelf_id()); |
| |
| if (item_controller != nullptr && item_controller->window_count() == 0) { |
| ash::ShelfID shelf_id = item_controller->shelf_id(); |
| owner()->CloseLauncherItem(shelf_id); |
| return shelf_id; |
| } |
| return ash::ShelfID(); |
| } |
| |
| void CrostiniAppWindowShelfController::ActiveUserChanged( |
| const std::string& user_email) { |
| for (auto& w : aura_window_to_app_window_) { |
| if (MultiUserWindowManager::GetInstance() |
| ->GetWindowOwner(w.first) |
| .GetUserEmail() == user_email) { |
| AddToShelf(w.first, w.second.get()); |
| } else { |
| RemoveFromShelf(w.first, w.second.get()); |
| } |
| } |
| } |
| |
| void CrostiniAppWindowShelfController::OnWindowInitialized( |
| aura::Window* window) { |
| // An Crostini window has type WINDOW_TYPE_NORMAL, a WindowDelegate and |
| // is a top level views widget. Tooltips, menus, and other kinds of transient |
| // windows that can't activate are filtered out. The transient child is set |
| // up after window Init so add it here but remove it later. |
| if (window->type() != aura::client::WINDOW_TYPE_NORMAL || !window->delegate()) |
| return; |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); |
| if (!widget || !widget->is_top_level()) |
| return; |
| if (!widget->CanActivate()) |
| return; |
| |
| observed_windows_.emplace(window); |
| window->AddObserver(this); |
| } |
| |
| void CrostiniAppWindowShelfController::OnWindowVisibilityChanging( |
| aura::Window* window, |
| bool visible) { |
| if (!visible) |
| return; |
| |
| // Transient windows are set up after window Init, so remove them here. |
| if (wm::GetTransientParent(window)) { |
| DCHECK(aura_window_to_app_window_.find(window) == |
| aura_window_to_app_window_.end()); |
| auto it = observed_windows_.find(window); |
| DCHECK(it != observed_windows_.end()); |
| observed_windows_.erase(it); |
| window->RemoveObserver(this); |
| return; |
| } |
| |
| // Skip when this window has been handled. This can happen when the window |
| // becomes visible again. |
| auto app_window_it = aura_window_to_app_window_.find(window); |
| if (app_window_it != aura_window_to_app_window_.end()) |
| return; |
| |
| // Handle browser windows, such as the Crostini terminal. |
| Browser* browser = chrome::FindBrowserWithWindow(window); |
| if (browser) { |
| base::Optional<std::string> app_id = |
| crostini::CrostiniAppIdFromAppName(browser->app_name()); |
| if (!app_id) |
| return; |
| RegisterAppWindow(window, app_id.value()); |
| return; |
| } |
| |
| // Handle genuine Crostini app windows. |
| const std::string* window_app_id = |
| exo::ShellSurface::GetApplicationId(window); |
| |
| crostini::CrostiniRegistryService* registry_service = |
| crostini::CrostiniRegistryServiceFactory::GetForProfile( |
| owner()->profile()); |
| const std::string& shelf_app_id = registry_service->GetCrostiniShelfAppId( |
| window_app_id, exo::ShellSurface::GetStartupId(window)); |
| // Non-crostini apps (i.e. arc++) are filtered out here. |
| if (shelf_app_id.empty()) |
| return; |
| |
| // Failed to uniquely identify the Crostini app that this window is for. |
| // The spinners on the shelf have internal app IDs which are valid |
| // extensions IDs. If the ID here starts with "crostini:" then it implies |
| // that it has failed to identify the exact app that's starting. |
| // The existing spinner that fails to be linked back should be closed, |
| // otherwise it will be left on the shelf indefinetely until it is closed |
| // manually by the user. |
| // When the condition is triggered here, the container is up and at least |
| // one app is starting. It's safe to close all the spinners since their |
| // respective apps take at most another few seconds to start. |
| // Work is ongoing to make this occur as infrequently as possible. |
| // See https://crbug.com/854911. |
| if (base::StartsWith(shelf_app_id, crostini::kCrostiniAppIdPrefix, |
| base::CompareCase::SENSITIVE)) { |
| owner()->GetShelfSpinnerController()->CloseCrostiniSpinners(); |
| } |
| |
| RegisterAppWindow(window, shelf_app_id); |
| |
| // Prevent Crostini window from showing up after user switch. |
| MultiUserWindowManager::GetInstance()->SetWindowOwner( |
| window, |
| user_manager::UserManager::Get()->GetActiveUser()->GetAccountId()); |
| |
| // Move the Crostini app window to the right display if necessary. |
| int64_t display_id = crostini_app_display_.GetDisplayIdForAppId(shelf_app_id); |
| if (display_id == display::kInvalidDisplayId) |
| return; |
| |
| display::Display new_display; |
| if (!display::Screen::GetScreen()->GetDisplayWithDisplayId(display_id, |
| &new_display)) |
| return; |
| display::Display old_display = |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window); |
| |
| if (new_display != old_display) |
| MoveWindowFromOldDisplayToNewDisplay(window, old_display, new_display); |
| } |
| |
| void CrostiniAppWindowShelfController::RegisterAppWindow( |
| aura::Window* window, |
| const std::string& shelf_app_id) { |
| const ash::ShelfID shelf_id(shelf_app_id); |
| views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window); |
| aura_window_to_app_window_[window] = |
| std::make_unique<AppWindowBase>(shelf_id, widget); |
| AppWindowBase* app_window = aura_window_to_app_window_[window].get(); |
| AddToShelf(window, app_window); |
| } |
| |
| void CrostiniAppWindowShelfController::OnWindowDestroying( |
| aura::Window* window) { |
| auto it = observed_windows_.find(window); |
| DCHECK(it != observed_windows_.end()); |
| observed_windows_.erase(it); |
| window->RemoveObserver(this); |
| |
| auto app_window_it = aura_window_to_app_window_.find(window); |
| if (app_window_it == aura_window_to_app_window_.end()) |
| return; |
| |
| ash::ShelfID shelf_id = RemoveFromShelf(window, app_window_it->second.get()); |
| if (!shelf_id.IsNull()) { |
| const std::string& app_id = shelf_id.app_id; |
| if (app_id == app_id_to_restart_) { |
| crostini::LaunchCrostiniApp( |
| ChromeLauncherController::instance()->profile(), app_id, |
| display_id_to_restart_in_); |
| app_id_to_restart_.clear(); |
| } |
| } |
| |
| aura_window_to_app_window_.erase(app_window_it); |
| } |
| |
| AppWindowLauncherItemController* |
| CrostiniAppWindowShelfController::ControllerForWindow(aura::Window* window) { |
| if (!window) |
| return nullptr; |
| |
| auto app_window_it = aura_window_to_app_window_.find(window); |
| if (app_window_it == aura_window_to_app_window_.end()) |
| return nullptr; |
| |
| AppWindowBase* app_window = app_window_it->second.get(); |
| |
| if (app_window == nullptr) |
| return nullptr; |
| |
| return app_window->controller(); |
| } |
| |
| void CrostiniAppWindowShelfController::UnregisterAppWindow( |
| AppWindowBase* app_window) { |
| if (!app_window) |
| return; |
| |
| AppWindowLauncherItemController* controller = app_window->controller(); |
| if (controller) |
| controller->RemoveWindow(app_window); |
| app_window->SetController(nullptr); |
| } |
| |
| void CrostiniAppWindowShelfController::OnItemDelegateDiscarded( |
| ash::ShelfItemDelegate* delegate) { |
| for (auto& it : aura_window_to_app_window_) { |
| AppWindowBase* app_window = it.second.get(); |
| if (!app_window || app_window->controller() != delegate) |
| continue; |
| |
| VLOG(1) << "Item controller was released externally for the app " |
| << delegate->shelf_id().app_id << "."; |
| |
| UnregisterAppWindow(it.second.get()); |
| } |
| } |
| |
| void CrostiniAppWindowShelfController::OnAppLaunchRequested( |
| const std::string& app_id, |
| int64_t display_id) { |
| crostini_app_display_.Register(app_id, display_id); |
| } |
| |
| void CrostiniAppWindowShelfController::Restart(const ash::ShelfID& shelf_id, |
| int64_t display_id) { |
| app_id_to_restart_ = shelf_id.app_id; |
| display_id_to_restart_in_ = display_id; |
| ChromeLauncherController::instance()->Close(shelf_id); |
| } |