chromium/chromeos/ash/components/game_mode/game_mode_controller.cc

// 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 "chromeos/ash/components/game_mode/game_mode_controller.h"

#include "ash/components/arc/arc_util.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/borealis/borealis_util.h"
#include "chromeos/ash/components/dbus/resourced/resourced_client.h"
#include "ui/views/widget/widget.h"

namespace game_mode {

namespace {

constexpr int kRefreshSec = 60;
constexpr int kTimeoutSec = kRefreshSec + 10;

}  // 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);
}

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;

  if (ash::borealis::IsBorealisWindow(window)) {
    focused_ = std::make_unique<WindowTracker>(
        window_state, std::move(maybe_keep_focused),
        base::BindRepeating(
            &GameModeController::NotifySetGameMode,
            // This is safe because the callback is only used by WindowTracker
            // and GameModeEnabler, which are owned by (or transitively owned
            // by) GameModeController. Therefore |this| cannot be stale.
            base::Unretained(this)));
  }
}

GameModeController::WindowTracker::WindowTracker(
    ash::WindowState* window_state,
    std::unique_ptr<WindowTracker> previous_focus,
    NotifySetGameModeCallback notify_set_game_mode_callback)
    : notify_set_game_mode_callback_(notify_set_game_mode_callback) {
  auto* window = window_state->window();

  // Maintain game mode state without dropping momentarily out of it
  // when switching between full-screen game windows.
  if (previous_focus) {
    // Enabler will be null if game mode is not on before focus switch.
    game_mode_enabler_ = std::move(previous_focus->game_mode_enabler_);
  }

  window_state_observer_.Observe(window_state);
  window_observer_.Observe(window);

  // If maintaining game mode state, update the window reference on the reused
  // enabler. Must occur after observers are setup.
  if (game_mode_enabler_) {
    game_mode_enabler_->SetWindowState(window_state);
  }

  // This possibly turns OFF as well as turns on game mode.
  UpdateGameModeStatus(window_state);
}

GameModeController::WindowTracker::~WindowTracker() {}

void GameModeController::WindowTracker::OnPostWindowStateTypeChange(
    ash::WindowState* window_state,
    chromeos::WindowStateType old_type) {
  UpdateGameModeStatus(window_state);
}

void GameModeController::WindowTracker::UpdateGameModeStatus(
    ash::WindowState* window_state) {
  auto* window = window_state->window();
  CHECK(ash::borealis::IsBorealisWindow(window));

  if (!window_state->IsFullscreen()) {
    game_mode_enabler_.reset();
    return;
  }

  if (game_mode_enabler_) {
    // No need to create a new enabler. The existing one is already valid for
    // this window.
    return;
  }

  VLOG(2) << "Initializing GameModeEnabler for Borealis";

  // Borealis has no further criteria than the window being fullscreen and
  // focused, already guaranteed by WindowTracker existing.
  DCHECK_EQ(window_state, window_state_observer_.GetSource());
  game_mode_enabler_ = std::make_unique<GameModeEnabler>(
      window_state, notify_set_game_mode_callback_);
}

void GameModeController::WindowTracker::OnWindowDestroying(
    aura::Window* window) {
  window_state_observer_.Reset();
  window_observer_.Reset();
  game_mode_enabler_.reset();
}

bool GameModeController::GameModeEnabler::should_record_failure;

GameModeController::GameModeEnabler::GameModeEnabler(
    ash::WindowState* window_state,
    NotifySetGameModeCallback notify_set_game_mode_callback)
    : window_state_(window_state),
      notify_set_game_mode_callback_(notify_set_game_mode_callback) {
  notify_set_game_mode_callback_.Run(GameMode::BOREALIS, window_state_);

  GameModeEnabler::should_record_failure = true;
  base::UmaHistogramEnumeration(kGameModeResultHistogramName,
                                GameModeResult::kAttempted);
  if (ash::ResourcedClient::Get()) {
    ash::ResourcedClient::Get()->SetGameModeWithTimeout(
        GameMode::BOREALIS, kTimeoutSec,
        base::BindOnce(&GameModeEnabler::OnSetGameMode,
                       /*refresh_of=*/std::nullopt));
  }
  timer_.Start(FROM_HERE, base::Seconds(kRefreshSec), this,
               &GameModeEnabler::RefreshGameMode);
}

GameModeController::GameModeEnabler::~GameModeEnabler() {
  auto time_in_mode = began_.Elapsed();
  base::UmaHistogramLongTimes100(kTimeInGameModeHistogramName, time_in_mode);

  notify_set_game_mode_callback_.Run(GameMode::OFF, window_state_);

  timer_.Stop();
  VLOG(1) << "Turning off game mode";
  if (ash::ResourcedClient::Get()) {
    ash::ResourcedClient::Get()->SetGameModeWithTimeout(
        GameMode::OFF, 0,
        base::BindOnce(&GameModeEnabler::OnSetGameMode, GameMode::BOREALIS));
  }
}

void GameModeController::GameModeEnabler::SetWindowState(
    ash::WindowState* window_state) {
  if (window_state_ == window_state) {
    return;
  }

  window_state_ = window_state;
  notify_set_game_mode_callback_.Run(GameMode::BOREALIS, window_state_);
}

void GameModeController::GameModeEnabler::RefreshGameMode() {
  if (ash::ResourcedClient::Get()) {
    ash::ResourcedClient::Get()->SetGameModeWithTimeout(
        GameMode::BOREALIS, kTimeoutSec,
        base::BindOnce(&GameModeEnabler::OnSetGameMode, GameMode::BOREALIS));
  }
}

// Previous is whether game mode was enabled previous to this call.
void GameModeController::GameModeEnabler::OnSetGameMode(
    std::optional<GameMode> refresh_of,
    std::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(kGameModeResultHistogramName,
                                  GameModeResult::kFailed);
    // Only record failures once per entry into game mode.
    GameModeEnabler::should_record_failure = false;
  }
}

void GameModeController::NotifySetGameMode(GameMode game_mode,
                                           ash::WindowState* window_state) {
  if (!callback_.is_null()) {
    callback_.Run(window_state->window(), game_mode);
  }
}

}  // namespace game_mode