0

CrOS Game Dashboard: initial commit

Initial commit of controller and stubbed out session.

The controller is only instantiated if kGameDashboard feature flag
is enabled and on a test image. The latter requirement will be
relaxed once the feature is further into development.

Technical design doc: go/cros-game-dashboard-mvp-tdd

Bug: http://b/270150206
Test: ash_unittests --gtest_filter=GameDashboard*
Change-Id: Ieb82274b7a8351cf054d378a21d8105e104383c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4178993
Commit-Queue: Brad McKee <bradmckee@google.com>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1115211}
This commit is contained in:
Brad McKee
2023-03-09 18:08:53 +00:00
committed by Chromium LUCI CQ
parent 3bb6447614
commit edde24d0e7
9 changed files with 392 additions and 2 deletions

@ -573,6 +573,10 @@ component("ash") {
"frame_throttler/frame_throttling_controller.cc",
"frame_throttler/frame_throttling_controller.h",
"frame_throttler/frame_throttling_observer.h",
"game_dashboard/game_dashboard_controller.cc",
"game_dashboard/game_dashboard_controller.h",
"game_dashboard/game_dashboard_session.cc",
"game_dashboard/game_dashboard_session.h",
"glanceables/glanceables_controller.cc",
"glanceables/glanceables_controller.h",
"glanceables/glanceables_delegate.h",
@ -2935,6 +2939,7 @@ test("ash_unittests") {
"frame_sink/frame_sink_holder_unittest.cc",
"frame_sink/ui_resource_manager_unittest.cc",
"frame_throttler/frame_throttling_controller_unittest.cc",
"game_dashboard/game_dashboard_unittest.cc",
"glanceables/glanceables_unittests.cc",
"glanceables/glanceables_welcome_label_unittest.cc",
"glanceables/signout_screenshot_handler_unittest.cc",

@ -22,6 +22,7 @@
#include "ash/display/screen_orientation_controller.h"
#include "ash/focus_cycler.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/keyboard/keyboard_controller_impl.h"
#include "ash/media/media_controller_impl.h"
@ -508,7 +509,7 @@ bool CanToggleGameDashboard() {
return false;
}
aura::Window* window = window_util::GetActiveWindow();
return window && IsArcWindow(window);
return window && GameDashboardController::CanStart(window);
}
bool CanToggleMultitaskMenu() {
@ -1316,7 +1317,12 @@ void ToggleGameDashboard() {
DCHECK(features::IsGameDashboardEnabled());
aura::Window* window = window_util::GetActiveWindow();
DCHECK(window);
// TODO(phshah): Connect to the game dashboard controller.
auto* controller = Shell::Get()->game_dashboard_controller();
if (!controller->IsActive(window)) {
controller->Start(window);
} else {
controller->ToggleMenu(window);
}
}
void ToggleHighContrast() {

@ -0,0 +1,116 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/game_dashboard/game_dashboard_controller.h"
#include <memory>
#include <string>
#include "ash/game_dashboard/game_dashboard_session.h"
#include "ash/public/cpp/app_types_util.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/logging.h"
namespace ash {
namespace {
// Gets top level window of the provided window if the top level window is not
// null. Otherwise return the window.
aura::Window* GetTopLevelWindow(aura::Window* window) {
return window ? window->GetToplevelWindow() : nullptr;
}
} // namespace
GameDashboardController::GameDashboardController() {
Shell::Get()->session_controller()->AddObserver(this);
}
GameDashboardController::~GameDashboardController() {
ShutdownAllSessions();
Shell::Get()->session_controller()->RemoveObserver(this);
}
// static
bool GameDashboardController::CanStart(aura::Window* window) {
return IsArcWindow(window);
}
bool GameDashboardController::IsActive(aura::Window* window) const {
auto it = sessions_.find(window);
return it != sessions_.end() && !it->second->is_shutting_down();
}
bool GameDashboardController::Start(aura::Window* window) {
window = GetTopLevelWindow(window);
if (!window) {
VLOG(1) << "Ignoring attempt to start game dashboard with a null window";
return false;
}
if (!CanStart(window)) {
return false;
}
auto& session = sessions_[window];
if (session) {
// Already exists.
return false;
}
session = std::make_unique<GameDashboardSession>(window);
session->Initialize();
window_observations_.AddObservation(window);
return true;
}
void GameDashboardController::Stop(aura::Window* window) {
auto it_session = sessions_.find(window);
if (it_session != sessions_.end()) {
window_observations_.RemoveObservation(window);
it_session->second->Shutdown();
sessions_.erase(it_session);
}
}
void GameDashboardController::ToggleMenu(aura::Window* window) {
auto it_session = sessions_.find(GetTopLevelWindow(window));
if (it_session != sessions_.end()) {
it_session->second->ToggleMenu();
}
}
void GameDashboardController::OnActiveUserSessionChanged(
const AccountId& account_id) {
ShutdownAllSessions();
}
void GameDashboardController::OnSessionStateChanged(
session_manager::SessionState state) {
if (Shell::Get()->session_controller()->IsUserSessionBlocked()) {
ShutdownAllSessions();
}
}
void GameDashboardController::OnChromeTerminating() {
ShutdownAllSessions();
}
void GameDashboardController::OnWindowDestroying(aura::Window* window) {
Stop(window);
}
void GameDashboardController::ShutdownAllSessions() {
window_observations_.RemoveAllObservations();
for (auto& it_session : sessions_) {
if (!it_session.second->is_shutting_down()) {
it_session.second->Shutdown();
}
}
sessions_.clear();
}
} // namespace ash

@ -0,0 +1,75 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONTROLLER_H_
#define ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONTROLLER_H_
#include <memory>
#include "ash/ash_export.h"
#include "ash/public/cpp/session/session_observer.h"
#include "base/containers/flat_map.h"
#include "base/scoped_multi_source_observation.h"
#include "ui/aura/window_observer.h"
namespace ash {
class GameDashboardSession;
// The game dashboard controller is responsible for creating and managing game
// dashboard sessions. It provides a means to start a session on the focused
// window, if that window is a relevant game surface. The session can also be
// stopped.
class ASH_EXPORT GameDashboardController : public SessionObserver,
public aura::WindowObserver {
public:
GameDashboardController();
GameDashboardController(const GameDashboardController&) = delete;
GameDashboardController& operator=(const GameDashboardController&) = delete;
~GameDashboardController() override;
// Returns true if this window supports starting the game dashboard. Otherwise
// returns false.
static bool CanStart(aura::Window* window);
// Returns true if there is an active game dashboard session associated
// with the given window. Otherwise returns false.
bool IsActive(aura::Window* window) const;
// If there is not an active game dashboard session for the given window,
// starts one and returns true. If there is already an active session, this
// method returns false.
bool Start(aura::Window* window);
// If there is an active game dashboard session for the given window, stops
// that session. Otherwise does nothing.
void Stop(aura::Window* window);
// If there is an active game dashboard session for the given window, toggles
// the associated game dashboard menu. Otherwise does nothing.
void ToggleMenu(aura::Window* window);
// SessionObserver:
void OnActiveUserSessionChanged(const AccountId& account_id) override;
void OnSessionStateChanged(session_manager::SessionState state) override;
void OnChromeTerminating() override;
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override;
private:
// Calls Shutdown on all active sessions and clears the session map. Generally
// this is done on logged in user change or Chrome terminating.
void ShutdownAllSessions();
base::flat_map<aura::Window*, std::unique_ptr<GameDashboardSession>>
sessions_;
base::ScopedMultiSourceObservation<aura::Window, aura::WindowObserver>
window_observations_{this};
};
} // namespace ash
#endif // ASH_GAME_DASHBOARD_GAME_DASHBOARD_CONTROLLER_H_

@ -0,0 +1,30 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/game_dashboard/game_dashboard_session.h"
#include "base/check.h"
namespace ash {
GameDashboardSession::GameDashboardSession(aura::Window* window)
: window_(window) {
DCHECK(window);
}
GameDashboardSession::~GameDashboardSession() = default;
void GameDashboardSession::Initialize() {
// Not yet implemented
}
void GameDashboardSession::Shutdown() {
is_shutting_down_ = true;
}
void GameDashboardSession::ToggleMenu() {
// Not yet implemented
}
} // namespace ash

@ -0,0 +1,49 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_GAME_DASHBOARD_GAME_DASHBOARD_SESSION_H_
#define ASH_GAME_DASHBOARD_GAME_DASHBOARD_SESSION_H_
namespace aura {
class Window;
}
namespace ash {
// A game dashboard session is created for an app that raised the
// game dashboard UI. It should be initialized with the top level
// window of the app in which the dashboard was raised. The session
// may continue even when all visual elements are closed if there is
// active screen recording in progress.
class GameDashboardSession {
public:
explicit GameDashboardSession(aura::Window* window);
GameDashboardSession(const GameDashboardSession&) = delete;
GameDashboardSession& operator=(const GameDashboardSession&) = delete;
~GameDashboardSession();
bool is_shutting_down() const { return is_shutting_down_; }
aura::Window* window() const { return window_; }
// Creates all UI elements that might be needed for this session.
void Initialize();
// `Shutdown()` should be called just before the session destructor is called.
void Shutdown();
// Toggles the main menu.
void ToggleMenu();
private:
// The top window associated with this session.
aura::Window* const window_;
// Once the `Shutdown()` method has been called on this session, this value
// will be true.
bool is_shutting_down_ = false;
};
} // namespace ash
#endif // ASH_GAME_DASHBOARD_GAME_DASHBOARD_SESSION_H_

@ -0,0 +1,93 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/constants/app_types.h"
#include "ash/constants/ash_features.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/system/sys_info.h"
#include "base/test/scoped_feature_list.h"
#include "ui/aura/client/aura_constants.h"
namespace ash {
class GameDashboardTest : public AshTestBase {
protected:
GameDashboardTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~GameDashboardTest() override = default;
void SetUp() override {
base::SysInfo::SetChromeOSVersionInfoForTest(
"CHROMEOS_RELEASE_TRACK=testimage-channel",
base::SysInfo::GetLsbReleaseTime());
scoped_feature_list_.InitAndEnableFeature({features::kGameDashboard});
AshTestBase::SetUp();
EXPECT_TRUE(features::IsGameDashboardEnabled());
}
void TearDown() override {
AshTestBase::TearDown();
base::SysInfo::ResetChromeOSVersionInfoForTest();
}
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(GameDashboardTest, StartStopController) {
GameDashboardController* controller =
Shell::Get()->game_dashboard_controller();
ASSERT_TRUE(controller);
// Without a focused game window, the controller shouldn't start.
controller->Start(nullptr);
EXPECT_FALSE(controller->IsActive(nullptr));
// With a non-ARC app window, the controller shouldn't start, even if start is
// requested.
auto owned_browser_window =
CreateAppWindow(gfx::Rect(5, 5, 20, 20), ash::AppType::BROWSER);
aura::Window* browser_window = owned_browser_window.get();
EXPECT_FALSE(controller->IsActive(browser_window));
controller->Start(browser_window);
EXPECT_FALSE(controller->IsActive(browser_window));
// Make sure it is safe to try to stop an inactive controller.
controller->Stop(browser_window);
EXPECT_FALSE(controller->IsActive(browser_window));
// With an active ARC window, the controller should start.
auto owned_arc_window =
CreateAppWindow(gfx::Rect(5, 5, 20, 20), ash::AppType::ARC_APP);
aura::Window* arc_window = owned_arc_window.get();
controller->Start(arc_window);
EXPECT_TRUE(controller->IsActive(arc_window));
// Make sure an active controller stops.
controller->Stop(arc_window);
EXPECT_FALSE(controller->IsActive(arc_window));
// Test start following a stop.
controller->Start(arc_window);
EXPECT_TRUE(controller->IsActive(arc_window));
}
TEST_F(GameDashboardTest, DestroyWindow) {
GameDashboardController* controller =
Shell::Get()->game_dashboard_controller();
ASSERT_TRUE(controller);
// With an active ARC window, the controller should start.
auto owned_window =
CreateAppWindow(gfx::Rect(5, 5, 20, 20), ash::AppType::ARC_APP);
aura::Window* window = owned_window.get();
controller->Start(window);
EXPECT_TRUE(controller->IsActive(window));
owned_window.reset();
EXPECT_FALSE(controller->IsActive(window));
}
} // namespace ash

@ -71,6 +71,7 @@
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/frame/snap_controller_impl.h"
#include "ash/frame_throttler/frame_throttling_controller.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/glanceables/glanceables_controller.h"
#include "ash/glanceables/glanceables_delegate.h"
#include "ash/host/ash_window_tree_host_init_params.h"
@ -837,6 +838,8 @@ Shell::~Shell() {
window_cycle_controller_.reset();
overview_controller_.reset();
game_dashboard_controller_.reset();
// This must be destroyed before deleting all the windows below in
// `CloseAllRootWindowChildWindows()`, since shutting down the session will
// need to access those windows and it will be a UAF.
@ -1165,6 +1168,10 @@ void Shell::Init(
capture_mode_controller_ = std::make_unique<CaptureModeController>(
shell_delegate_->CreateCaptureModeDelegate());
if (features::IsGameDashboardEnabled()) {
game_dashboard_controller_ = std::make_unique<GameDashboardController>();
}
// Accelerometer file reader starts listening to tablet mode controller.
AccelerometerReader::GetInstance()->StartListenToTabletModeController();

@ -12,6 +12,7 @@
#include "ash/accessibility/accessibility_event_handler_manager.h"
#include "ash/ash_export.h"
#include "ash/constants/ash_features.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/in_session_auth/in_session_auth_dialog_controller_impl.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/public/cpp/shelf_types.h"
@ -250,6 +251,10 @@ namespace federated {
class FederatedServiceControllerImpl;
} // namespace federated
namespace game_dashboard {
class GameDashboardController;
} // namespace game_dashboard
namespace quick_pair {
class Mediator;
} // namespace quick_pair
@ -507,6 +512,9 @@ class ASH_EXPORT Shell : public SessionObserver,
FullscreenMagnifierController* fullscreen_magnifier_controller() {
return fullscreen_magnifier_controller_.get();
}
GameDashboardController* game_dashboard_controller() {
return game_dashboard_controller_.get();
}
GeolocationController* geolocation_controller() {
return geolocation_controller_.get();
}
@ -908,6 +916,7 @@ class ASH_EXPORT Shell : public SessionObserver,
firmware_update_notification_controller_;
std::unique_ptr<FocusCycler> focus_cycler_;
std::unique_ptr<FloatController> float_controller_;
std::unique_ptr<GameDashboardController> game_dashboard_controller_;
std::unique_ptr<GeolocationController> geolocation_controller_;
std::unique_ptr<GlanceablesController> glanceables_controller_;
std::unique_ptr<HoldingSpaceController> holding_space_controller_;