// Copyright 2020 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/ash/borealis/borealis_window_manager.h"
#include <string>
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "borealis_util.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_util.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.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/profiles/profile.h"
#include "chromeos/ash/components/borealis/borealis_util.h"
#include "components/exo/shell_surface_util.h"
#include "components/prefs/pref_service.h"
namespace borealis {
const char kBorealisClientSuffix[] = "wmclass.Steam";
const char kBorealisAnonymousPrefix[] = "borealis_anon:";
const int kSteamClientGameId = 769;
namespace {
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(std::string, kShelfAppIdKey, nullptr)
// Returns an ID for this window, as set by the Wayland client that created it.
//
// Prefers the value set via xdg_toplevel.set_app_id(), if any. Falls back to
// the value set via zaura_surface.set_startup_id().
// The ID string is owned by the window.
const std::string* WaylandWindowId(const aura::Window* window) {
const std::string* id = exo::GetShellApplicationId(window);
if (id)
return id;
return exo::GetShellStartupId(window);
}
// Return the GuestOS Shelf App ID of an installed app with the given Steam Game
// ID.
//
// Relies on the Exec line in the desktop entry (.desktop file within the VM)
// having the expected format.
std::string SteamGameIdToShelfAppId(Profile* profile, unsigned steam_game_id) {
for (const auto& item :
guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile)
->GetRegisteredApps(guest_os::VmType::BOREALIS)) {
std::optional<int> app_id = ParseSteamGameId(item.second.Exec());
if (app_id && app_id.value() == static_cast<int>(steam_game_id)) {
return item.first;
}
}
return {};
}
// Return the GuestOS Shelf App ID for the given window.
std::string ShelfAppId(Profile* profile, const aura::Window* window) {
// The Steam Game ID is the most reliable method, if known.
std::optional<int> steam_id = SteamGameId(window);
if (steam_id.has_value()) {
if (steam_id.value() == kSteamClientGameId) {
return kClientAppId;
}
std::string app_id = SteamGameIdToShelfAppId(profile, steam_id.value());
if (!app_id.empty()) {
return app_id;
}
}
// Fall back to GuestOS's logic for associating windows with apps.
// GetGuestOsShelfAppId will handle both registered and anonymous borealis app
// windows correctly. For registered apps, it will return a matching shelf app
// ID. For unregistered apps, it will return an app_id prefixed with
// "borealis_anon:".
// TODO(cpelling): Log a warning here once all Steam startup windows and
// games are correctly registered.
return guest_os::GetGuestOsShelfAppId(profile, WaylandWindowId(window),
nullptr);
}
} // namespace
bool BorealisWindowManager::IsSteamGameWindow(Profile* profile,
const aura::Window* window) {
// Only windows from the Borealis VM can possibly be Steam games.
if (!ash::borealis::IsBorealisWindow(window)) {
return false;
}
// Exclude selected windows that are not games, such as Steam client windows.
std::string shelf_app_id = ShelfAppId(profile, window);
if (IsNonGameBorealisApp(shelf_app_id)) {
return false;
}
// TODO(b/289158975): Exclude game launcher windows.
// Every other Borealis window with the STEAM_GAME property is a game.
return SteamGameId(window).has_value();
}
// static
bool BorealisWindowManager::IsAnonymousAppId(const std::string& app_id) {
return base::StartsWith(app_id, kBorealisAnonymousPrefix,
base::CompareCase::SENSITIVE);
}
BorealisWindowManager::BorealisWindowManager(Profile* profile)
: profile_(profile), instance_registry_observation_(this) {}
BorealisWindowManager::~BorealisWindowManager() {
for (auto& observer : anon_observers_) {
observer.OnWindowManagerDeleted(this);
}
for (auto& observer : lifetime_observers_) {
observer.OnWindowManagerDeleted(this);
}
DCHECK(anon_observers_.empty());
DCHECK(lifetime_observers_.empty());
}
void BorealisWindowManager::AddObserver(AnonymousAppObserver* observer) {
anon_observers_.AddObserver(observer);
}
void BorealisWindowManager::RemoveObserver(AnonymousAppObserver* observer) {
anon_observers_.RemoveObserver(observer);
}
void BorealisWindowManager::AddObserver(AppWindowLifetimeObserver* observer) {
lifetime_observers_.AddObserver(observer);
}
void BorealisWindowManager::RemoveObserver(
AppWindowLifetimeObserver* observer) {
lifetime_observers_.RemoveObserver(observer);
}
std::string BorealisWindowManager::GetShelfAppId(aura::Window* window) {
if (!ash::borealis::IsBorealisWindow(window)) {
return {};
}
// We delay the observation until the first time we actually see a borealis
// window, which prevents unnecessary messages being sent and breaks an
// init-cycle.
if (!instance_registry_observation_.IsObserving()) {
instance_registry_observation_.Observe(
&apps::AppServiceProxyFactory::GetForProfile(profile_)
->InstanceRegistry());
}
if (!window->GetProperty(kShelfAppIdKey))
window->SetProperty(kShelfAppIdKey, ShelfAppId(profile_, window));
return *window->GetProperty(kShelfAppIdKey);
}
void BorealisWindowManager::OnInstanceUpdate(
const apps::InstanceUpdate& update) {
aura::Window* window = update.Window();
if (!ash::borealis::IsBorealisWindow(window)) {
return;
}
if (update.IsCreation()) {
HandleWindowCreation(window, update.AppId());
} else if (update.IsDestruction()) {
HandleWindowDestruction(window, update.AppId());
}
}
void BorealisWindowManager::OnInstanceRegistryWillBeDestroyed(
apps::InstanceRegistry* cache) {
DCHECK(instance_registry_observation_.IsObservingSource(cache));
instance_registry_observation_.Reset();
}
void BorealisWindowManager::HandleWindowDestruction(aura::Window* window,
const std::string& app_id) {
for (auto& observer : lifetime_observers_) {
observer.OnWindowFinished(app_id, window);
}
base::flat_map<
std::string,
base::flat_set<raw_ptr<aura::Window, CtnExperimental>>>::iterator iter =
ids_to_windows_.find(app_id);
DCHECK(iter != ids_to_windows_.end());
DCHECK(iter->second.contains(window));
iter->second.erase(window);
if (!iter->second.empty())
return;
if (IsAnonymousAppId(app_id)) {
for (auto& observer : anon_observers_)
observer.OnAnonymousAppRemoved(app_id);
}
for (auto& observer : lifetime_observers_)
observer.OnAppFinished(app_id, window);
ids_to_windows_.erase(iter);
if (!ids_to_windows_.empty())
return;
for (auto& observer : lifetime_observers_)
observer.OnSessionFinished();
}
void BorealisWindowManager::HandleWindowCreation(aura::Window* window,
const std::string& app_id) {
// If this is the first window, the session has started.
if (ids_to_windows_.empty()) {
for (auto& observer : lifetime_observers_)
observer.OnSessionStarted();
}
// If this is the given app_id's first window, the app has started
if (ids_to_windows_[app_id].empty()) {
for (auto& observer : lifetime_observers_)
observer.OnAppStarted(app_id);
if (IsAnonymousAppId(app_id)) {
for (auto& observer : anon_observers_)
observer.OnAnonymousAppAdded(app_id,
base::UTF16ToUTF8(window->GetTitle()));
}
}
// If this window was not already in the set, notify our observers about it.
if (ids_to_windows_[app_id].emplace(window).second) {
for (auto& observer : lifetime_observers_)
observer.OnWindowStarted(app_id, window);
}
}
} // namespace borealis