// Copyright 2019 The Chromium Authors
// 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/shelf/app_service/app_service_app_window_crostini_tracker.h"
#include "ash/components/arc/arc_util.h"
#include "ash/public/cpp/multi_user_window_manager.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/wm/window_state.h"
#include "base/containers/flat_tree.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/borealis/borealis_window_manager.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_force_close_watcher.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_shelf_utils.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/app_window_base.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/common/chrome_features.h"
#include "chromeos/ash/components/borealis/borealis_util.h"
#include "components/exo/permission.h"
#include "components/exo/shell_surface_util.h"
#include "components/user_manager/user_manager.h"
#include "ui/aura/window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/wm/core/window_util.h"
namespace {
// Time allowed for apps to self-activate after launch, see
// go/crostini-self-activate for details.
constexpr base::TimeDelta kSelfActivationTimeout = base::Seconds(5);
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);
}
// Returns true if the crostini tracker should ignore this window. Mainly used
// to exclude other windows that are created by exo. Transient windows may
// actually belong to crostini but we exclude them as well as their IDs will be
// set at a later point.
bool ShouldSkipWindow(aura::Window* window) {
return wm::GetTransientParent(window) ||
arc::GetWindowTaskOrSessionId(window).has_value() ||
crosapi::browser_util::IsLacrosWindow(window) ||
plugin_vm::IsPluginVmAppWindow(window) ||
ash::borealis::IsBorealisWindow(window);
}
} // namespace
AppServiceAppWindowCrostiniTracker::AppServiceAppWindowCrostiniTracker(
AppServiceAppWindowShelfController* app_service_controller)
: app_service_controller_(app_service_controller) {}
AppServiceAppWindowCrostiniTracker::~AppServiceAppWindowCrostiniTracker() =
default;
void AppServiceAppWindowCrostiniTracker::OnWindowVisibilityChanged(
aura::Window* window,
const std::string& shelf_app_id) {
if (ShouldSkipWindow(window))
return;
// Handle browser windows.
Browser* browser = chrome::FindBrowserWithWindow(window);
if (browser)
return;
// Currently Crostini can only be used from the primary profile. In the
// future, this may be replaced by some way of matching the container that
// runs this app with the user that owns it.
const AccountId& primary_account_id =
user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId();
Profile* primary_account_profile =
ash::ProfileHelper::Get()->GetProfileByAccountId(primary_account_id);
// Windows without an application id set will get filtered out here.
const std::string& crostini_shelf_app_id = guest_os::GetGuestOsShelfAppId(
primary_account_profile, exo::GetShellApplicationId(window),
exo::GetShellStartupId(window));
if (crostini_shelf_app_id.empty())
return;
auto* registry_service =
guest_os::GuestOsRegistryServiceFactory::GetForProfile(
primary_account_profile);
// At this point, all remaining windows are Crostini windows. Firstly, we add
// support for forcibly closing it. We use the registration to retrieve the
// app's name, but this may be null in the case of apps with no associated
// launcher entry (i.e. no .desktop file), in which case the app's name is
// unknown.
std::optional<guest_os::GuestOsRegistryService::Registration> registration =
registry_service->GetRegistration(shelf_app_id);
RegisterCrostiniWindowForForceClose(
window, registration.has_value() ? registration->Name() : "");
// 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 (guest_os::IsUnregisteredCrostiniShelfAppId(shelf_app_id)) {
ChromeShelfController::instance()
->GetShelfSpinnerController()
->CloseCrostiniSpinners();
}
// Prevent Crostini window from showing up after user switch.
MultiUserWindowManagerHelper::GetWindowManager()->SetWindowOwner(
window, primary_account_id);
// 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 registered_display;
if (!display::Screen::GetScreen()->GetDisplayWithDisplayId(
display_id, ®istered_display)) {
return;
}
display::Display current_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
if (registered_display != current_display) {
auto* state = ash::WindowState::Get(window);
// 'window' is about to unminimize.
if (state && !state->IsMinimized()) {
MoveWindowFromOldDisplayToNewDisplay(window, current_display,
registered_display);
// 'window' is about to minimize. Therefore we take this opportunity to
// re-register it to the current display it is shown upon.
} else {
crostini_app_display_.Register(shelf_app_id, current_display.id());
}
}
}
void AppServiceAppWindowCrostiniTracker::OnWindowDestroying(
aura::Window* window) {
activation_permissions_.erase(window);
}
void AppServiceAppWindowCrostiniTracker::OnAppLaunchRequested(
const std::string& app_id,
int64_t display_id) {
crostini_app_display_.Register(app_id, display_id);
// Remove the old permissions and add a permission for every window the app
// currently has open.
for (aura::Window* window : activation_permissions_)
exo::RevokePermissionToActivate(window);
activation_permissions_.clear();
ash::ShelfModel* model = app_service_controller_->owner()->shelf_model();
int index = model->ItemIndexByAppID(app_id);
if (index >= static_cast<int>(model->items().size()) || index < 0)
return;
AppWindowShelfItemController* item_controller =
model->GetAppWindowShelfItemController(model->items()[index].id);
// Apps run for the first time won't have a launcher controller yet, return
// early because they won't have windows either so permissions aren't
// necessary.
if (!item_controller)
return;
for (AppWindowBase* app_window : item_controller->windows()) {
exo::GrantPermissionToActivate(app_window->GetNativeWindow(),
kSelfActivationTimeout);
activation_permissions_.insert(app_window->GetNativeWindow());
}
}
std::string AppServiceAppWindowCrostiniTracker::GetShelfAppId(
aura::Window* window) const {
if (ShouldSkipWindow(window))
return std::string();
// Handle browser windows.
Browser* browser = chrome::FindBrowserWithWindow(window);
if (browser)
return std::string();
// Currently Crostini can only be used from the primary profile. In the
// future, this may be replaced by some way of matching the container that
// runs this app with the user that owns it.
Profile* primary_account_profile =
ash::ProfileHelper::Get()->GetProfileByAccountId(
user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId());
std::string shelf_app_id = guest_os::GetGuestOsShelfAppId(
primary_account_profile, exo::GetShellApplicationId(window),
exo::GetShellStartupId(window));
// When install a new Crostini app and run it directly, Crostini might not get
// the correct app id yet when `window` is created, but use an unregistered
// app id for a short term. Then the unregistered app id is saved in
// InstanceRegistry for `window`. So when the app id is set for `window`
// later, the app id inconsistent DCHECK is hit, which could affect the
// instance saved in InstanceRegistry. To prevent the updating for `window` in
// InstanceRegistry, call MaybeModifyInstance to check the saved app id and
// the expected shelf_app_id, and if they are not consistent, modify the app
// id to use `shelf_app_id`.
if (!shelf_app_id.empty())
MaybeModifyInstance(primary_account_profile, window, shelf_app_id);
return shelf_app_id;
}
void AppServiceAppWindowCrostiniTracker::RegisterCrostiniWindowForForceClose(
aura::Window* window,
const std::string& app_name) {
exo::ShellSurfaceBase* surface = exo::GetShellSurfaceBaseForWindow(window);
if (!surface)
return;
crostini::ForceCloseWatcher::Watch(
std::make_unique<crostini::ShellSurfaceForceCloseDelegate>(surface,
app_name));
}
void AppServiceAppWindowCrostiniTracker::MaybeModifyInstance(
Profile* profile,
aura::Window* window,
const std::string& app_id) const {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
DCHECK(proxy);
auto& instance_registry = proxy->InstanceRegistry();
std::string old_app_id = instance_registry.GetShelfId(window).app_id;
if (old_app_id.empty() || app_id == old_app_id)
return;
auto* app_service_instance_helper =
app_service_controller_->app_service_instance_helper();
DCHECK(app_service_instance_helper);
auto state = instance_registry.GetState(window);
app_service_instance_helper->OnInstances(old_app_id, window, std::string(),
apps::InstanceState::kDestroyed);
app_service_instance_helper->OnInstances(app_id, window, std::string(),
state);
}