0

Create UserEducationController in Ash with browser delegate.

This CL only stubs in the basic architecture, but the controller is
intended to be used to register/start Ash feature tutorials, the most
immediate of which being a "Welcome Tour" with more to follow.

This effectively exposes an existing user education framework in the
browser to Ash.

See proof of concept for a better idea of intended use:
https://chromium-review.googlesource.com/c/chromium/src/+/4295679

Bug: b:272844012
Change-Id: Ia473c7bf00e31927c375e4f041089ac6e6a517d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4330599
Reviewed-by: Dana Fried <dfried@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Commit-Queue: David Black <dmblack@google.com>
Cr-Commit-Position: refs/heads/main@{#1116623}
This commit is contained in:
David Black
2023-03-13 21:59:09 +00:00
committed by Chromium LUCI CQ
parent bf92087510
commit b8609fa606
24 changed files with 359 additions and 0 deletions

@ -851,6 +851,9 @@ component("ash") {
"projector/ui/projector_button.h",
"projector/ui/projector_color_button.cc",
"projector/ui/projector_color_button.h",
"user_education/user_education_controller.cc",
"user_education/user_education_controller.h",
"user_education/user_education_delegate.h",
"wm/multi_display/persistent_window_controller.cc",
"wm/multi_display/persistent_window_controller.h",
"wm/multi_display/persistent_window_info.cc",
@ -3329,6 +3332,7 @@ test("ash_unittests") {
"tray_action/test_tray_action_client.cc",
"tray_action/test_tray_action_client.h",
"tray_action/tray_action_unittest.cc",
"user_education/user_education_controller_unittest.cc",
"utility/cropping_util_unittest.cc",
"utility/haptics_util_unittest.cc",
"utility/layer_copy_animator_unittest.cc",

@ -2122,6 +2122,9 @@ BASE_FEATURE(kWebUITabStripTabDragIntegration,
"WebUITabStripTabDragIntegration",
base::FEATURE_ENABLED_BY_DEFAULT);
// Enables the Welcome Tour that walks new users through ChromeOS System UI.
BASE_FEATURE(kWelcomeTour, "WelcomeTour", base::FEATURE_DISABLED_BY_DEFAULT);
// Controls whether to enable MAC Address Randomization on WiFi connection.
BASE_FEATURE(kWifiConnectMacAddressRandomization,
"WifiConnectMacAddressRandomization",
@ -3230,6 +3233,10 @@ bool IsWebUITabStripTabDragIntegrationEnabled() {
return base::FeatureList::IsEnabled(kWebUITabStripTabDragIntegration);
}
bool IsWelcomeTourEnabled() {
return base::FeatureList::IsEnabled(kWelcomeTour);
}
bool IsWifiSyncAndroidEnabled() {
return base::FeatureList::IsEnabled(kWifiSyncAndroid);
}

@ -604,6 +604,8 @@ COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kWallpaperPerDesk);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWebUITabStripTabDragIntegration);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWelcomeTour);
COMPONENT_EXPORT(ASH_CONSTANTS)
BASE_DECLARE_FEATURE(kWifiConnectMacAddressRandomization);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kWifiSyncAllowDeletes);
COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kWifiSyncAndroid);
@ -886,6 +888,7 @@ COMPONENT_EXPORT(ASH_CONSTANTS)
bool IsWallpaperGooglePhotosSharedAlbumsEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWallpaperPerDeskEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWebUITabStripTabDragIntegrationEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWelcomeTourEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWifiSyncAndroidEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS) bool IsWmModeEnabled();
COMPONENT_EXPORT(ASH_CONSTANTS)

@ -170,6 +170,8 @@
#include "ash/touch/touch_devices_controller.h"
#include "ash/touch/touch_selection_magnifier_runner_ash.h"
#include "ash/tray_action/tray_action.h"
#include "ash/user_education/user_education_controller.h"
#include "ash/user_education/user_education_delegate.h"
#include "ash/utility/occlusion_tracker_pauser.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wm/ash_focus_rules.h"
@ -1606,6 +1608,11 @@ void Shell::Init(
std::make_unique<federated::FederatedServiceControllerImpl>();
}
if (features::IsWelcomeTourEnabled()) {
user_education_controller_ = std::make_unique<UserEducationController>(
shell_delegate_->CreateUserEducationDelegate());
}
// Injects the factory which fulfills the implementation of the text context
// menu exclusive to CrOS.
views::ViewsTextServicesContextMenuChromeos::SetImplFactory(

@ -232,6 +232,7 @@ class ClipboardHistoryControllerImpl;
class TouchDevicesController;
class TouchSelectionMagnifierRunnerAsh;
class TrayAction;
class UserEducationController;
class UserMetricsRecorder;
class VideoActivityNotifier;
class VideoDetector;
@ -993,6 +994,7 @@ class ASH_EXPORT Shell : public SessionObserver,
std::unique_ptr<ClipboardHistoryControllerImpl> clipboard_history_controller_;
std::unique_ptr<TouchDevicesController> touch_devices_controller_;
std::unique_ptr<TrayAction> tray_action_;
std::unique_ptr<UserEducationController> user_education_controller_;
std::unique_ptr<WallpaperControllerImpl> wallpaper_controller_;
std::unique_ptr<WindowCycleController> window_cycle_controller_;
std::unique_ptr<WindowRestoreController> window_restore_controller_;

@ -42,6 +42,7 @@ class NearbyShareController;
class NearbyShareDelegate;
class SavedDeskDelegate;
class SystemSoundsDelegate;
class UserEducationDelegate;
// Delegate of the Shell.
class ASH_EXPORT ShellDelegate {
@ -87,6 +88,10 @@ class ASH_EXPORT ShellDelegate {
virtual std::unique_ptr<SystemSoundsDelegate> CreateSystemSoundsDelegate()
const = 0;
// Creates and returns the delegate for user education features.
virtual std::unique_ptr<UserEducationDelegate> CreateUserEducationDelegate()
const = 0;
// Returns the geolocation loader factory used to initialize geolocation
// provider.
virtual scoped_refptr<network::SharedURLLoaderFactory>

@ -69,6 +69,11 @@ TestShellDelegate::CreateSystemSoundsDelegate() const {
return std::make_unique<TestSystemSoundsDelegate>();
}
std::unique_ptr<UserEducationDelegate>
TestShellDelegate::CreateUserEducationDelegate() const {
return nullptr;
}
scoped_refptr<network::SharedURLLoaderFactory>
TestShellDelegate::GetGeolocationUrlLoaderFactory() const {
return static_cast<scoped_refptr<network::SharedURLLoaderFactory>>(

@ -50,6 +50,8 @@ class TestShellDelegate : public ShellDelegate {
std::unique_ptr<SavedDeskDelegate> CreateSavedDeskDelegate() const override;
std::unique_ptr<SystemSoundsDelegate> CreateSystemSoundsDelegate()
const override;
std::unique_ptr<UserEducationDelegate> CreateUserEducationDelegate()
const override;
scoped_refptr<network::SharedURLLoaderFactory>
GetGeolocationUrlLoaderFactory() const override;
bool CanGoBack(gfx::NativeWindow window) const override;

@ -0,0 +1,13 @@
# Metadata information for this directory.
#
# For more information on DIR_METADATA files, see:
# https://chromium.googlesource.com/chromiumos/docs/+/HEAD/dir_metadata.md
#
# For the schema of this file, see Metadata message:
# https://source.chromium.org/chromium/infra/infra/+/HEAD:go/src/infra/tools/dirmd/proto/dir_metadata.proto
buganizer: {
component_id: 1330383 # ChromeOS > Software > System UI Surfaces > UserEducation
}
team_email: "cros-system-ui-productivity-eng@google.com"

@ -0,0 +1 @@
mixins: "//ash/user_education/COMMON_METADATA"

@ -0,0 +1 @@
dmblack@google.com

@ -0,0 +1,37 @@
// 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/user_education/user_education_controller.h"
#include "ash/user_education/user_education_delegate.h"
#include "base/check_op.h"
namespace ash {
namespace {
// The singleton instance owned by `Shell`.
UserEducationController* g_instance = nullptr;
} // namespace
// UserEducationController -----------------------------------------------------
UserEducationController::UserEducationController(
std::unique_ptr<UserEducationDelegate> delegate)
: delegate_(std::move(delegate)) {
DCHECK_EQ(g_instance, nullptr);
g_instance = this;
}
UserEducationController::~UserEducationController() {
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
UserEducationController* UserEducationController::Get() {
return g_instance;
}
} // namespace ash

@ -0,0 +1,35 @@
// 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_USER_EDUCATION_USER_EDUCATION_CONTROLLER_H_
#define ASH_USER_EDUCATION_USER_EDUCATION_CONTROLLER_H_
#include <memory>
#include "ash/ash_export.h"
namespace ash {
class UserEducationDelegate;
// The controller, owned by `Shell`, for user education features in Ash.
class ASH_EXPORT UserEducationController {
public:
explicit UserEducationController(std::unique_ptr<UserEducationDelegate>);
UserEducationController(const UserEducationController&) = delete;
UserEducationController& operator=(const UserEducationController&) = delete;
~UserEducationController();
// Returns the singleton instance owned by `Shell`.
static UserEducationController* Get();
private:
// The delegate which facilitates communication between Ash and user
// education services in the browser.
std::unique_ptr<UserEducationDelegate> delegate_;
};
} // namespace ash
#endif // ASH_USER_EDUCATION_USER_EDUCATION_CONTROLLER_H_

@ -0,0 +1,50 @@
// 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/user_education/user_education_controller.h"
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
// UserEducationControllerTest -------------------------------------------------
// Base class for tests of the `UserEducationController` parameterized by
// whether the Welcome Tour feature is enabled.
class UserEducationControllerTest
: public AshTestBase,
public testing::WithParamInterface</*welcome_tour_enabled=*/bool> {
public:
UserEducationControllerTest() {
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
(IsWelcomeTourEnabled() ? enabled_features : disabled_features)
.emplace_back(features::kWelcomeTour);
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
// Returns whether the Welcome Tour is enabled given test parameterization.
bool IsWelcomeTourEnabled() const { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All,
UserEducationControllerTest,
/*welcome_tour_enabled=*/testing::Bool());
// Tests -----------------------------------------------------------------------
// Verifies that the controller exists if and only if Welcome Tour is enabled.
TEST_P(UserEducationControllerTest, Exists) {
EXPECT_EQ(!!UserEducationController::Get(), IsWelcomeTourEnabled());
}
} // namespace ash

@ -0,0 +1,36 @@
// 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_USER_EDUCATION_USER_EDUCATION_DELEGATE_H_
#define ASH_USER_EDUCATION_USER_EDUCATION_DELEGATE_H_
#include <string>
#include "ash/ash_export.h"
class AccountId;
namespace user_education {
struct TutorialDescription;
} // namespace user_education
namespace ash {
// The delegate of the `UserEducationController` which facilitates communication
// between Ash and user education services in the browser.
class ASH_EXPORT UserEducationDelegate {
public:
virtual ~UserEducationDelegate() = default;
// Registers the tutorial defined by the specified `tutorial_id` and
// `tutorial_description` for the user associated with the given `account_id`.
virtual void RegisterTutorial(
const AccountId& account_id,
const std::string& tutorial_id,
user_education::TutorialDescription tutorial_description) = 0;
};
} // namespace ash
#endif // ASH_USER_EDUCATION_USER_EDUCATION_DELEGATE_H_

@ -2547,6 +2547,8 @@ static_library("ui") {
"ash/touch_selection_menu_chromeos.h",
"ash/touch_selection_menu_runner_chromeos.cc",
"ash/touch_selection_menu_runner_chromeos.h",
"ash/user_education/chrome_user_education_delegate.cc",
"ash/user_education/chrome_user_education_delegate.h",
"ash/vpn_list_forwarder.cc",
"ash/vpn_list_forwarder.h",
"ash/wallpaper_controller_client_impl.cc",

@ -43,6 +43,7 @@
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_ui.h"
#include "chrome/browser/ui/ash/session_util.h"
#include "chrome/browser/ui/ash/system_sounds_delegate_impl.h"
#include "chrome/browser/ui/ash/user_education/chrome_user_education_delegate.h"
#include "chrome/browser/ui/ash/window_pin_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_command_controller.h"
@ -169,6 +170,11 @@ ChromeShellDelegate::CreateSystemSoundsDelegate() const {
return std::make_unique<SystemSoundsDelegateImpl>();
}
std::unique_ptr<ash::UserEducationDelegate>
ChromeShellDelegate::CreateUserEducationDelegate() const {
return std::make_unique<ChromeUserEducationDelegate>();
}
scoped_refptr<network::SharedURLLoaderFactory>
ChromeShellDelegate::GetGeolocationUrlLoaderFactory() const {
return g_browser_process->shared_url_loader_factory();

@ -39,6 +39,8 @@ class ChromeShellDelegate : public ash::ShellDelegate {
const override;
std::unique_ptr<ash::SystemSoundsDelegate> CreateSystemSoundsDelegate()
const override;
std::unique_ptr<ash::UserEducationDelegate> CreateUserEducationDelegate()
const override;
scoped_refptr<network::SharedURLLoaderFactory>
GetGeolocationUrlLoaderFactory() const override;
void OpenKeyboardShortcutHelpPage() const override;

@ -0,0 +1 @@
mixins: "//ash/user_education/COMMON_METADATA"

@ -0,0 +1 @@
file://ash/user_education/OWNERS

@ -0,0 +1,28 @@
// 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 "chrome/browser/ui/ash/user_education/chrome_user_education_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/user_education/user_education_service.h"
#include "chrome/browser/ui/user_education/user_education_service_factory.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/account_id/account_id.h"
#include "components/user_education/common/tutorial_registry.h"
ChromeUserEducationDelegate::ChromeUserEducationDelegate() = default;
ChromeUserEducationDelegate::~ChromeUserEducationDelegate() = default;
void ChromeUserEducationDelegate::RegisterTutorial(
const AccountId& account_id,
const std::string& tutorial_id,
user_education::TutorialDescription tutorial_description) {
UserEducationServiceFactory::GetForProfile(
Profile::FromBrowserContext(
ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
account_id)))
->tutorial_registry()
.AddTutorial(tutorial_id, std::move(tutorial_description));
}

@ -0,0 +1,28 @@
// 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 CHROME_BROWSER_UI_ASH_USER_EDUCATION_CHROME_USER_EDUCATION_DELEGATE_H_
#define CHROME_BROWSER_UI_ASH_USER_EDUCATION_CHROME_USER_EDUCATION_DELEGATE_H_
#include "ash/user_education/user_education_delegate.h"
// The delegate of the `UserEducationController` which facilitates communication
// between Ash and user education services in the browser.
class ChromeUserEducationDelegate : public ash::UserEducationDelegate {
public:
ChromeUserEducationDelegate();
ChromeUserEducationDelegate(const ChromeUserEducationDelegate&) = delete;
ChromeUserEducationDelegate& operator=(const ChromeUserEducationDelegate&) =
delete;
~ChromeUserEducationDelegate() override;
private:
// ash::UserEducationDelegate:
void RegisterTutorial(
const AccountId& account_id,
const std::string& tutorial_id,
user_education::TutorialDescription tutorial_description) override;
};
#endif // CHROME_BROWSER_UI_ASH_USER_EDUCATION_CHROME_USER_EDUCATION_DELEGATE_H_

@ -0,0 +1,82 @@
// 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 "chrome/browser/ui/ash/user_education/chrome_user_education_delegate.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/test/ash_test_helper.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ui/user_education/user_education_service.h"
#include "chrome/browser/ui/user_education/user_education_service_factory.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/user_education/common/tutorial_description.h"
#include "components/user_education/common/tutorial_registry.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user.h"
#include "testing/gtest/include/gtest/gtest.h"
// ChromeUserEducationDelegateTest ---------------------------------------------
// Base class for tests of the `ChromeUserEducationDelegate`.
class ChromeUserEducationDelegateTest : public BrowserWithTestWindowTest {
public:
ChromeUserEducationDelegateTest()
: user_manager_(new ash::FakeChromeUserManager()),
user_manager_enabler_(base::WrapUnique(user_manager_)) {}
// Returns a pointer to the `delegate_` instance under test.
ash::UserEducationDelegate* delegate() { return &delegate_; }
private:
// BrowserWithTestWindowTest:
TestingProfile* CreateProfile() override {
constexpr char kUserEmail[] = "user@test";
const AccountId kUserAccountId(AccountId::FromUserEmail(kUserEmail));
// Register user.
user_manager_->AddUser(kUserAccountId);
user_manager_->LoginUser(kUserAccountId);
// Activate session.
auto* client = ash_test_helper()->test_session_controller_client();
client->AddUserSession(kUserEmail);
client->SwitchActiveUser(kUserAccountId);
// Create profile.
return profile_manager()->CreateTestingProfile(kUserEmail);
}
// User management.
ash::FakeChromeUserManager* const user_manager_;
user_manager::ScopedUserManager user_manager_enabler_;
// The delegate instance under test.
ChromeUserEducationDelegate delegate_;
};
// Tests -----------------------------------------------------------------------
// Verifies `RegisterTutorial()` registers a tutorial with the browser registry.
TEST_F(ChromeUserEducationDelegateTest, RegisterTutorial) {
constexpr char kTutorialId[] = "Tutorial ID";
// Initially there should be no tutorial registered.
user_education::TutorialRegistry& tutorial_registry =
UserEducationServiceFactory::GetForProfile(profile())
->tutorial_registry();
EXPECT_FALSE(tutorial_registry.IsTutorialRegistered(kTutorialId));
// Attempt to register a tutorial.
delegate()->RegisterTutorial(ash::BrowserContextHelper::Get()
->GetUserByBrowserContext(profile())
->GetAccountId(),
kTutorialId,
user_education::TutorialDescription());
// Confirm tutorial registration.
EXPECT_TRUE(tutorial_registry.IsTutorialRegistered(kTutorialId));
}

@ -7588,6 +7588,7 @@ test("unit_tests") {
"../browser/ui/ash/shelf/chrome_shelf_controller_unittest.cc",
"../browser/ui/ash/shelf/chrome_shelf_prefs_unittest.cc",
"../browser/ui/ash/shelf/shelf_context_menu_unittest.cc",
"../browser/ui/ash/user_education/chrome_user_education_delegate_unittest.cc",
"../browser/ui/ash/wallpaper_controller_client_impl_unittest.cc",
"../browser/ui/ash/window_pin_util_unittest.cc",
"../browser/ui/quick_answers/quick_answers_controller_unittest.cc",