| // 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/game_mode/game_mode_controller.h" |
| |
| #include "ash/components/arc/arc_features.h" |
| #include "ash/components/arc/arc_util.h" |
| #include "ash/components/arc/mojom/app.mojom.h" |
| #include "ash/components/arc/session/connection_holder.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/shell.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h" |
| #include "chrome/browser/ash/arc/arc_util.h" |
| #include "chrome/browser/ash/borealis/borealis_window_manager.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chromeos/ash/components/dbus/resourced/resourced_client.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace game_mode { |
| |
| using borealis::BorealisWindowManager; |
| |
| namespace { |
| |
| constexpr int kRefreshSec = 60; |
| constexpr int kTimeoutSec = kRefreshSec + 10; |
| |
| // A GameModeCriteria for ARC windows. This potentially owns a GameModeEnabler |
| // which is initialized if the task of the window is determined to be a game. |
| class ArcGameModeCriteria : public GameModeController::GameModeCriteria { |
| public: |
| // Constructs an instance using the task ID of the window it is associated |
| // with. |
| explicit ArcGameModeCriteria(aura::Window* window) { |
| // For ARC container boards, Game Mode optimizations are not available |
| // (b/248972198). |
| if (!arc::IsArcVmEnabled()) |
| return; |
| |
| // ARC is only allowed for the primary user. |
| auto* profile = ProfileManager::GetPrimaryUserProfile(); |
| DCHECK(arc::IsArcAllowedForProfile(profile)); |
| |
| connection_ = ArcAppListPrefs::Get(profile)->app_connection_holder(); |
| auto* pkg_name = window->GetProperty(ash::kArcPackageNameKey); |
| |
| if (!pkg_name || pkg_name->empty()) { |
| LOG(ERROR) << "Failed to find package name for the requested task"; |
| return; |
| } |
| |
| if (IsKnownGame(*pkg_name)) { |
| VLOG(2) << "ARC task package " << pkg_name << " is known game"; |
| Enable(); |
| return; |
| } |
| |
| auto* app_instance = |
| ARC_GET_INSTANCE_FOR_METHOD(connection_.get(), GetAppCategory); |
| if (!app_instance) |
| return; |
| VLOG(2) << "Fetch app category of package: " << pkg_name; |
| |
| app_instance->GetAppCategory( |
| *pkg_name, base::BindOnce(&ArcGameModeCriteria::OnReceiveAppCategory, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Checks if an ARC game package is in the known games list. These are apps |
| // which should be treated like a game without checking their actual app |
| // category. These games may not be classified as games in their manifest, but |
| // we want to treat them as games.. |
| static bool IsKnownGame(const std::string& pkg_name) { |
| // This does not have a category set as of v1.18.32 (circa late Sept, 2022). |
| if (pkg_name == "com.mojang.minecraftedu") |
| return true; |
| |
| // Not sure about whether the app already has the correct category, as I do |
| // not have access to the APK. As we have no way to alter the list between |
| // release milestones, add it just in case. |
| if (pkg_name == "com.mojang.minecraftpe") |
| return true; |
| |
| return false; |
| } |
| |
| void OnReceiveAppCategory(arc::mojom::AppCategory category) { |
| VLOG(2) << "ARC app category is: " << category; |
| if (category == arc::mojom::AppCategory::kGame) { |
| Enable(); |
| } |
| } |
| |
| GameMode mode() const override { return GameMode::ARC; } |
| |
| private: |
| void Enable() { |
| bool signal_resourced = base::FeatureList::IsEnabled(arc::kGameModeFeature); |
| enabler_ = std::make_unique<GameModeController::GameModeEnabler>( |
| GameMode::ARC, signal_resourced); |
| } |
| |
| raw_ptr<arc::ConnectionHolder<arc::mojom::AppInstance, arc::mojom::AppHost>, |
| ExperimentalAsh> |
| connection_; |
| |
| std::unique_ptr<GameModeController::GameModeEnabler> enabler_; |
| |
| // This must come last to make sure weak pointers are invalidated first. |
| base::WeakPtrFactory<ArcGameModeCriteria> weak_ptr_factory_{this}; |
| }; |
| |
| } // namespace |
| |
| GameModeController::GameModeController() { |
| if (!ash::Shell::HasInstance()) |
| return; |
| aura::client::FocusClient* focus_client = |
| aura::client::GetFocusClient(ash::Shell::GetPrimaryRootWindow()); |
| focus_client->AddObserver(this); |
| // In case a window is already focused when this is constructed. |
| OnWindowFocused(focus_client->GetFocusedWindow(), nullptr); |
| } |
| |
| GameModeController::~GameModeController() { |
| if (ash::Shell::HasInstance()) |
| aura::client::GetFocusClient(ash::Shell::GetPrimaryRootWindow()) |
| ->RemoveObserver(this); |
| } |
| |
| GameMode GameModeController::ModeOfWindow(aura::Window* window) { |
| if (BorealisWindowManager::IsBorealisWindow(window)) |
| return GameMode::BOREALIS; |
| |
| if (arc::GetWindowTaskId(window)) |
| return GameMode::ARC; |
| |
| return GameMode::OFF; |
| } |
| |
| void GameModeController::OnWindowFocused(aura::Window* gained_focus, |
| aura::Window* lost_focus) { |
| auto maybe_keep_focused = std::move(focused_); |
| |
| if (!gained_focus) |
| return; |
| |
| auto* widget = views::Widget::GetTopLevelWidgetForNativeView(gained_focus); |
| // |widget| can be nullptr in tests. |
| if (!widget) |
| return; |
| |
| aura::Window* window = widget->GetNativeWindow(); |
| auto* window_state = ash::WindowState::Get(window); |
| |
| if (!window_state) |
| return; |
| |
| auto mode = ModeOfWindow(window); |
| VLOG(4) << "Focused window game mode type: " << static_cast<int>(mode); |
| if (mode != GameMode::OFF) |
| focused_ = std::make_unique<WindowTracker>(window_state, |
| std::move(maybe_keep_focused)); |
| } |
| |
| GameModeController::WindowTracker::WindowTracker( |
| ash::WindowState* window_state, |
| std::unique_ptr<WindowTracker> previous_focus) { |
| auto* window = window_state->window(); |
| auto mode = ModeOfWindow(window); |
| |
| // Only Borealis mode can retain GameMode state without leaving, since ARC |
| // needs to fetch information after creating the GameModeCriteria instance. |
| if (previous_focus && mode == GameMode::BOREALIS) { |
| auto previous_criteria = std::move(previous_focus->game_mode_criteria_); |
| if (previous_criteria && previous_criteria->mode() == mode) |
| game_mode_criteria_ = std::move(previous_criteria); |
| } |
| |
| UpdateGameModeStatus(window_state); |
| window_state_observer_.Observe(window_state); |
| window_observer_.Observe(window); |
| } |
| |
| GameModeController::WindowTracker::~WindowTracker() {} |
| |
| void GameModeController::WindowTracker::OnPostWindowStateTypeChange( |
| ash::WindowState* window_state, |
| chromeos::WindowStateType old_type) { |
| UpdateGameModeStatus(window_state); |
| } |
| |
| GameMode GameModeController::GameModeEnabler::mode() const { |
| return mode_; |
| } |
| |
| void GameModeController::WindowTracker::UpdateGameModeStatus( |
| ash::WindowState* window_state) { |
| auto* window = window_state->window(); |
| auto mode = ModeOfWindow(window); |
| |
| if (!window_state->IsFullscreen() || mode == GameMode::OFF) { |
| game_mode_criteria_.reset(); |
| return; |
| } |
| |
| if (game_mode_criteria_) { |
| // No need to create a new criteria. The existing one is already valid for |
| // this window. |
| return; |
| } |
| |
| VLOG(2) << "Initializing GameModeCriteria for mode: " |
| << static_cast<int>(mode); |
| |
| if (mode == GameMode::BOREALIS) { |
| // Borealis has no further criteria than the window being fullscreen and |
| // focused, already guaranteed by WindowTracker existing. |
| game_mode_criteria_ = std::make_unique<GameModeEnabler>( |
| GameMode::BOREALIS, /*signal_resourced=*/true); |
| } else if (mode == GameMode::ARC) { |
| game_mode_criteria_ = std::make_unique<ArcGameModeCriteria>(window); |
| } else { |
| LOG(DFATAL) << "Unknown GameMode: " << static_cast<int>(mode); |
| } |
| } |
| |
| void GameModeController::WindowTracker::OnWindowDestroying( |
| aura::Window* window) { |
| window_state_observer_.Reset(); |
| window_observer_.Reset(); |
| game_mode_criteria_.reset(); |
| } |
| |
| bool GameModeController::GameModeEnabler::should_record_failure; |
| |
| GameModeController::GameModeEnabler::GameModeEnabler(GameMode mode, |
| bool signal_resourced) |
| : mode_(mode), signal_resourced_(signal_resourced) { |
| DCHECK(mode != GameMode::OFF); |
| |
| if (!signal_resourced) |
| return; |
| |
| GameModeEnabler::should_record_failure = true; |
| base::UmaHistogramEnumeration(GameModeResultHistogramName(mode), |
| GameModeResult::kAttempted); |
| if (ash::ResourcedClient::Get()) { |
| ash::ResourcedClient::Get()->SetGameModeWithTimeout( |
| mode_, kTimeoutSec, |
| base::BindOnce(&GameModeEnabler::OnSetGameMode, |
| /*refresh_of=*/absl::nullopt)); |
| } |
| timer_.Start(FROM_HERE, base::Seconds(kRefreshSec), this, |
| &GameModeEnabler::RefreshGameMode); |
| } |
| |
| GameModeController::GameModeEnabler::~GameModeEnabler() { |
| auto time_in_mode = began_.Elapsed(); |
| |
| base::UmaHistogramLongTimes100(TimeInGameModeHistogramName(mode_), |
| time_in_mode); |
| |
| if (!signal_resourced_) |
| return; |
| |
| timer_.Stop(); |
| VLOG(1) << "Turning off game mode type: " << static_cast<int>(mode_); |
| if (ash::ResourcedClient::Get()) { |
| ash::ResourcedClient::Get()->SetGameModeWithTimeout( |
| GameMode::OFF, 0, |
| base::BindOnce(&GameModeEnabler::OnSetGameMode, /*refresh_of=*/mode_)); |
| } |
| } |
| |
| void GameModeController::GameModeEnabler::RefreshGameMode() { |
| if (ash::ResourcedClient::Get()) { |
| ash::ResourcedClient::Get()->SetGameModeWithTimeout( |
| mode_, kTimeoutSec, |
| base::BindOnce(&GameModeEnabler::OnSetGameMode, /*refresh_of=*/mode_)); |
| } |
| } |
| |
| // Previous is whether game mode was enabled previous to this call. |
| void GameModeController::GameModeEnabler::OnSetGameMode( |
| absl::optional<GameMode> refresh_of, |
| absl::optional<GameMode> previous) { |
| if (!previous.has_value()) { |
| LOG(ERROR) << "Failed to set Game Mode"; |
| } else if (GameModeEnabler::should_record_failure && refresh_of.has_value() && |
| previous.value() != refresh_of.value()) { |
| // If game mode was not on and it was not the initial call, |
| // it means the previous call failed/timed out. |
| base::UmaHistogramEnumeration(GameModeResultHistogramName(*refresh_of), |
| GameModeResult::kFailed); |
| // Only record failures once per entry into gamemode. |
| GameModeEnabler::should_record_failure = false; |
| } |
| } |
| |
| } // namespace game_mode |