0

app-controls: Add Notification on First M127 Login

+ Notification only shows on first login after updating to M127, to
users for whom on-device app controls is available.
+ On click, the "Open Settings" button opens the Apps subpage
in settings.

Demo(DUT): http://screencast/cast/NTE1NDA2OTcyNjM2MzY0OHxmZGJkYzk0OC0xZA

Screenshot: https://screenshot.googleplex.com/8RPbdSVmmf6esKH
Bug: b:343223499
Change-Id: Ie58ff7e8678eca1331d6859009ad208998f0acf0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5604913
Commit-Queue: Amber Haynes <amberhaynes@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Yoshiki Iguchi <yoshiki@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Reviewed-by: Courtney Wong <courtneywong@chromium.org>
Reviewed-by: Aga Wronska <agawronska@chromium.org>
Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1314109}
This commit is contained in:
Amber Haynes
2024-06-12 17:43:29 +00:00
committed by Chromium LUCI CQ
parent 26193da841
commit e4b4258a85
18 changed files with 393 additions and 7 deletions

@ -1602,6 +1602,17 @@ You can also use the keyboard shortcut. First, highlight text, then press <ph na
This account is managed by Family Link
</message>
<!-- Strings for On-Device App Controls notification -->
<message name="IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_TITLE" desc="The title of the notification that informs the user that on-device app controls are available.">
Set up parental controls for apps
</message>
<message name="IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_MESSAGE" desc="The notification message that informs the user that on-device app controls are available.">
You can now block apps installed on this <ph name="DEVICE_TYPE">$1<ex>Chromebook</ex></ph>
</message>
<message name="IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_OPEN_SETTINGS_BUTTON_LABEL" desc="The label on the button that takes the user to the Apps Subpage of OS Settings.">
Open Settings
</message>
<message name="IDS_ASH_STATUS_TRAY_PREVIOUS_MENU" desc="The accessible text for header entries for detailed versions of status tray items.">
Previous menu
</message>

@ -0,0 +1 @@
51da6d8aa09dd42185beb45dd6fadad340275c1a

@ -0,0 +1 @@
51da6d8aa09dd42185beb45dd6fadad340275c1a

@ -0,0 +1 @@
51da6d8aa09dd42185beb45dd6fadad340275c1a

@ -219,6 +219,11 @@ inline constexpr char kEduCoexistenceToSVersion[] =
inline constexpr char kEduCoexistenceToSAcceptedVersion[] =
"family_link_user.edu_coexistence_tos_accepted_version";
// A boolean pref indicating if a notification about On Device App Controls
// has been shown to the user already.
inline constexpr char kOnDeviceAppControlsNotificationShown[] =
"on_device_app_controls.notification_shown";
// A string pref that stores the PIN used to unlock parental app controls.
inline constexpr char kOnDeviceAppControlsPin[] = "on_device_app_controls.pin";

@ -199,7 +199,8 @@ enum class NotificationCatalogName {
kGrowthFramework = 180,
kAudioSelection = 181,
kExtendedUpdatesAvailable = 182,
kMaxValue = kExtendedUpdatesAvailable
kOnDeviceAppControls = 183,
kMaxValue = kOnDeviceAppControls
};
// A living catalog that registers system nudges.

@ -808,6 +808,8 @@ source_set("ash") {
"child_accounts/on_device_controls/app_activity_watcher.cc",
"child_accounts/on_device_controls/app_activity_watcher.h",
"child_accounts/on_device_controls/app_controls_metrics_utils.h",
"child_accounts/on_device_controls/app_controls_notifier.cc",
"child_accounts/on_device_controls/app_controls_notifier.h",
"child_accounts/on_device_controls/app_controls_service.cc",
"child_accounts/on_device_controls/app_controls_service.h",
"child_accounts/on_device_controls/app_controls_service_factory.cc",
@ -5554,6 +5556,7 @@ source_set("unit_tests") {
"child_accounts/family_user_parental_control_metrics_unittest.cc",
"child_accounts/family_user_session_metrics_unittest.cc",
"child_accounts/on_device_controls/app_activity_watcher_unittest.cc",
"child_accounts/on_device_controls/app_controls_notifier_unittest.cc",
"child_accounts/on_device_controls/app_controls_service_factory_unittest.cc",
"child_accounts/on_device_controls/app_controls_test_base.cc",
"child_accounts/on_device_controls/app_controls_test_base.h",

@ -1,6 +1,7 @@
include_rules = [
# ChromeOS should not depend on //chrome. See //docs/chromeos/code.md for
# details.
# TODO(b/346593904): Cleanup Chrome dependencies in c/b/a/child_accounts.
"-chrome",
# This directory is in //chrome, which violates the rule above. Allow this
@ -16,6 +17,7 @@ include_rules = [
# directory basis. See //tools/chromeos/gen_deps.sh for details.
"+chrome/browser/ash",
"+chrome/browser/apps",
"+chrome/browser/notifications",
"+chrome/browser/policy",
"+chrome/browser/profiles",
"+chrome/test/base",
@ -27,4 +29,8 @@ specific_include_rules = {
"app_activity_watcher_unittest.cc": [
"+chrome/browser/ui/views/apps/app_dialog/app_local_block_dialog_view.h",
],
"app_controls_notifier.cc": [
"+chrome/browser/ui/settings_window_manager_chromeos.h",
"+ui/message_center/message_center.h",
],
}

@ -0,0 +1,128 @@
// Copyright 2024 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/child_accounts/on_device_controls/app_controls_notifier.h"
#include <memory>
#include <optional>
#include <string>
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/user_metrics.h"
#include "base/version_info/version_info.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/app_controls_service_factory.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "url/gurl.h"
namespace {
constexpr char kShowNotificationId[] = "show_app_controls_notification";
constexpr int kAppControlsMinimumVersion = 127;
// Action names should be kept in sync with corresponding actions in
// src/tools/metrics/actions/actions.xml.
constexpr char kNotificationClickedActionName[] =
"OnDeviceControls_NotificationClicked";
constexpr char kNotificationShownActionName[] =
"OnDeviceControls_NotificationShown";
} // namespace
namespace ash::on_device_controls {
// static
void AppControlsNotifier::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kOnDeviceAppControlsNotificationShown,
false);
}
AppControlsNotifier::AppControlsNotifier(Profile* profile)
: profile_(profile) {}
AppControlsNotifier::~AppControlsNotifier() = default;
void AppControlsNotifier::MaybeShowAppControlsNotification() {
if (!ShouldShowNotification()) {
return;
}
ShowNotification();
}
void AppControlsNotifier::HandleClick(std::optional<int> button_index) {
profile_->GetPrefs()->SetBoolean(prefs::kOnDeviceAppControlsNotificationShown,
true);
if (!button_index) {
return;
}
base::RecordAction(base::UserMetricsAction(kNotificationClickedActionName));
OpenAppsSettings();
NotificationDisplayService::GetForProfile(profile_)->Close(
NotificationHandler::Type::TRANSIENT, kShowNotificationId);
}
void AppControlsNotifier::OpenAppsSettings() {
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
profile_, chromeos::settings::mojom::kAppsSectionPath);
}
bool AppControlsNotifier::ShouldShowNotification() const {
if (!AppControlsServiceFactory::IsOnDeviceAppControlsAvailable(profile_)) {
return false;
}
// Skip notifying if the notification has been shown to the user before.
if (profile_->GetPrefs()->GetBoolean(
prefs::kOnDeviceAppControlsNotificationShown)) {
return false;
}
return version_info::GetMajorVersionNumberAsInt() >=
kAppControlsMinimumVersion;
}
void AppControlsNotifier::ShowNotification() {
std::u16string title =
l10n_util::GetStringUTF16(IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_TITLE);
std::u16string message = ui::SubstituteChromeOSDeviceType(
IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_MESSAGE);
message_center::RichNotificationData rich_notification_data;
rich_notification_data.buttons.emplace_back(l10n_util::GetStringUTF16(
IDS_ON_DEVICE_APP_CONTROLS_NOTIFICATION_OPEN_SETTINGS_BUTTON_LABEL));
message_center::Notification notification = ash::CreateSystemNotification(
message_center::NOTIFICATION_TYPE_SIMPLE, kShowNotificationId, title,
message, /*display_source=*/std::u16string(), /*origin_url=*/GURL(),
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
kShowNotificationId,
NotificationCatalogName::kOnDeviceAppControls),
rich_notification_data,
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(&AppControlsNotifier::HandleClick,
weak_ptr_factory_.GetWeakPtr())),
/*small_image=*/gfx::VectorIcon(),
message_center::SystemNotificationWarningLevel::NORMAL);
NotificationDisplayService::GetForProfile(profile_)->Display(
NotificationHandler::Type::TRANSIENT, notification,
/*metadata=*/nullptr);
base::RecordAction(base::UserMetricsAction(kNotificationShownActionName));
}
} // namespace ash::on_device_controls

@ -0,0 +1,51 @@
// Copyright 2024 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_ASH_CHILD_ACCOUNTS_ON_DEVICE_CONTROLS_APP_CONTROLS_NOTIFIER_H_
#define CHROME_BROWSER_ASH_CHILD_ACCOUNTS_ON_DEVICE_CONTROLS_APP_CONTROLS_NOTIFIER_H_
#include <optional>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
class PrefRegistrySimple;
class Profile;
namespace ash::on_device_controls {
// Displays and manages a notification informing eligible users that on-device
// app controls are available.
class AppControlsNotifier {
public:
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
explicit AppControlsNotifier(Profile* profile);
AppControlsNotifier(const AppControlsNotifier&) = delete;
AppControlsNotifier& operator=(const AppControlsNotifier&) = delete;
~AppControlsNotifier();
// Triggers a notification that app controls are available if the user is
// eligible and has not yet been shown the notification.
void MaybeShowAppControlsNotification();
private:
friend class AppControlsNotifierTest;
void HandleClick(std::optional<int> button_index);
void OpenAppsSettings();
bool ShouldShowNotification() const;
void ShowNotification();
const raw_ptr<Profile> profile_;
base::WeakPtrFactory<AppControlsNotifier> weak_ptr_factory_{this};
};
} // namespace ash::on_device_controls
#endif // CHROME_BROWSER_ASH_CHILD_ACCOUNTS_ON_DEVICE_CONTROLS_APP_CONTROLS_NOTIFIER_H_

@ -0,0 +1,145 @@
// Copyright 2024 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/child_accounts/on_device_controls/app_controls_notifier.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/test/test_system_tray_client.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/notifications/system_notification_helper.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "chromeos/ash/components/system/statistics_provider.h"
namespace {
constexpr char kEligibleDeviceRegionKey[] = "gp";
constexpr char kShowNotificationId[] = "show_app_controls_notification";
constexpr int kOpenSettingsButtonIndex = 0;
constexpr char kNotificationClickedActionName[] =
"OnDeviceControls_NotificationClicked";
constexpr char kNotificationShownActionName[] =
"OnDeviceControls_NotificationShown";
} // namespace
namespace ash::on_device_controls {
class AppControlsNotifierTest : public BrowserWithTestWindowTest {
public:
AppControlsNotifierTest() {
scoped_feature_list_.InitAndEnableFeature(features::kOnDeviceAppControls);
statistics_provider_.SetMachineStatistic(ash::system::kRegionKey,
kEligibleDeviceRegionKey);
}
AppControlsNotifierTest(const AppControlsNotifierTest&) = delete;
AppControlsNotifierTest& operator=(const AppControlsNotifierTest&) = delete;
~AppControlsNotifierTest() override = default;
// BrowserWithTestWindowTest:
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
std::make_unique<SystemNotificationHelper>());
tester_ = std::make_unique<NotificationDisplayServiceTester>(profile());
app_controls_notifier_ = std::make_unique<AppControlsNotifier>(profile());
}
void TearDown() override {
app_controls_notifier_.reset();
tester_.reset();
TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(nullptr);
BrowserWithTestWindowTest::TearDown();
}
AppControlsNotifier* app_controls_notifier() {
return app_controls_notifier_.get();
}
protected:
void ClickOpenSettingsButton() {
app_controls_notifier_->HandleClick(kOpenSettingsButtonIndex);
}
bool IsAppControlsNotificationPresent() const {
return tester_->GetNotification(kShowNotificationId).has_value();
}
base::test::ScopedFeatureList scoped_feature_list_;
private:
std::unique_ptr<AppControlsNotifier> app_controls_notifier_;
std::unique_ptr<NotificationDisplayServiceTester> tester_;
ash::system::ScopedFakeStatisticsProvider statistics_provider_;
};
TEST_F(AppControlsNotifierTest, ShowAppControlsNotification) {
base::UserActionTester user_action_tester;
app_controls_notifier()->MaybeShowAppControlsNotification();
EXPECT_TRUE(IsAppControlsNotificationPresent());
EXPECT_EQ(1, user_action_tester.GetActionCount(kNotificationShownActionName));
}
TEST_F(AppControlsNotifierTest, ClickAppControlsNotification) {
base::UserActionTester user_action_tester;
app_controls_notifier()->MaybeShowAppControlsNotification();
EXPECT_TRUE(IsAppControlsNotificationPresent());
ClickOpenSettingsButton();
EXPECT_EQ(1,
user_action_tester.GetActionCount(kNotificationClickedActionName));
// Notification should be removed on `Open Settings` click.
EXPECT_FALSE(IsAppControlsNotificationPresent());
}
TEST_F(AppControlsNotifierTest,
AppControlsNotificationDoesNotShowAgainAfterBeingClicked) {
base::UserActionTester user_action_tester;
EXPECT_FALSE(profile()->GetPrefs()->GetBoolean(
prefs::kOnDeviceAppControlsNotificationShown));
app_controls_notifier()->MaybeShowAppControlsNotification();
EXPECT_TRUE(IsAppControlsNotificationPresent());
ClickOpenSettingsButton();
EXPECT_TRUE(profile()->GetPrefs()->GetBoolean(
prefs::kOnDeviceAppControlsNotificationShown));
app_controls_notifier()->MaybeShowAppControlsNotification();
// Notification should have only been shown once.
EXPECT_EQ(1, user_action_tester.GetActionCount(kNotificationShownActionName));
}
class AppControlsNotifierDisabledTest : public AppControlsNotifierTest {
public:
AppControlsNotifierDisabledTest() {
scoped_feature_list_.Reset();
scoped_feature_list_.InitAndDisableFeature(features::kOnDeviceAppControls);
}
AppControlsNotifierDisabledTest(const AppControlsNotifierDisabledTest&) =
delete;
AppControlsNotifierDisabledTest& operator=(
const AppControlsNotifierDisabledTest&) = delete;
~AppControlsNotifierDisabledTest() override = default;
};
TEST_F(AppControlsNotifierDisabledTest,
AppControlsNotificationDoesNotShowWhenFeatureDisabled) {
app_controls_notifier()->MaybeShowAppControlsNotification();
EXPECT_FALSE(IsAppControlsNotificationPresent());
}
} // namespace ash::on_device_controls

@ -8,6 +8,7 @@
#include "ash/constants/ash_features.h"
#include "base/no_destructor.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/app_controls_notifier.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/app_controls_service.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/blocked_app_store.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/on_device_utils.h"
@ -65,6 +66,7 @@ AppControlsServiceFactory::BuildServiceInstanceForBrowserContext(
void AppControlsServiceFactory::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
AppControlsService::RegisterProfilePrefs(registry);
AppControlsNotifier::RegisterProfilePrefs(registry);
BlockedAppStore::RegisterProfilePrefs(registry);
}

@ -9,7 +9,9 @@
#include <vector>
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/app_controls_notifier.h"
#include "chrome/browser/ash/child_accounts/on_device_controls/blocked_app_registry.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/ash/settings/pages/apps/mojom/app_parental_controls_handler.mojom.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_types.h"
@ -38,16 +40,19 @@ bool ShouldIncludeApp(const apps::AppUpdate& update) {
AppParentalControlsHandler::AppParentalControlsHandler(
apps::AppServiceProxy* app_service_proxy,
PrefService* pref_service)
Profile* profile)
: app_service_proxy_(app_service_proxy),
app_controls_notifier_(
std::make_unique<on_device_controls::AppControlsNotifier>(profile)),
blocked_app_registry_(
std::make_unique<on_device_controls::BlockedAppRegistry>(
app_service_proxy,
pref_service)) {
profile->GetPrefs())) {
CHECK(app_service_proxy_);
CHECK(blocked_app_registry_);
app_registry_cache_observer_.Observe(&app_service_proxy_->AppRegistryCache());
app_controls_notifier_->MaybeShowAppControlsNotification();
}
AppParentalControlsHandler::~AppParentalControlsHandler() = default;

@ -17,11 +17,12 @@
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote_set.h"
class PrefService;
class Profile;
namespace ash {
namespace on_device_controls {
class AppControlsNotifier;
class BlockedAppRegistry;
} // namespace on_device_controls
@ -36,7 +37,7 @@ class AppParentalControlsHandler
public apps::AppRegistryCache::Observer {
public:
AppParentalControlsHandler(apps::AppServiceProxy* app_service_proxy,
PrefService* pref_service);
Profile* profile);
~AppParentalControlsHandler() override;
// app_parental_controls::mojom::AppParentalControlsHandler:
@ -69,6 +70,9 @@ class AppParentalControlsHandler
apps::AppRegistryCache::Observer>
app_registry_cache_observer_{this};
std::unique_ptr<on_device_controls::AppControlsNotifier>
app_controls_notifier_;
std::unique_ptr<on_device_controls::BlockedAppRegistry> blocked_app_registry_;
mojo::Receiver<app_parental_controls::mojom::AppParentalControlsHandler>

@ -120,7 +120,7 @@ class AppParentalControlsHandlerTest
on_device_controls::AppControlsTestBase::SetUp();
handler_ = std::make_unique<AppParentalControlsHandler>(
app_service_test().proxy(), profile().GetPrefs());
app_service_test().proxy(), &profile());
observer_ = std::make_unique<AppParentalControlsTestObserver>();
handler_->AddObserver(observer_->GenerateRemote());
}

@ -65,7 +65,7 @@ OsSettingsManager::OsSettingsManager(
std::make_unique<AppPermissionHandler>(app_service_proxy)),
app_parental_controls_handler_(
std::make_unique<AppParentalControlsHandler>(app_service_proxy,
profile->GetPrefs())),
profile)),
input_device_settings_provider_(
std::make_unique<InputDeviceSettingsProvider>()),
display_settings_provider_(std::make_unique<DisplaySettingsProvider>()),

@ -27557,6 +27557,27 @@ should be able to be added at any place in this file.
<description>Please enter the description of this user action.</description>
</action>
<action name="OnDeviceControls_NotificationClicked">
<owner>agawronska@chromium.org</owner>
<owner>courtneywong@chromium.org</owner>
<owner>amberhaynes@chromium.org</owner>
<owner>cros-families-eng@google.com</owner>
<description>
Recorded when a user clicks on the On-Device App Controls notification.
</description>
</action>
<action name="OnDeviceControls_NotificationShown">
<owner>agawronska@chromium.org</owner>
<owner>courtneywong@chromium.org</owner>
<owner>amberhaynes@chromium.org</owner>
<owner>cros-families-eng@google.com</owner>
<description>
Recorded when the On-Device App Controls notification is displayed to a
user.
</description>
</action>
<action name="Open_Sys_Internals">
<owner>yuholong@google.com</owner>
<owner>arcvm-eng@google.com</owner>

@ -27354,6 +27354,7 @@ Called by update_net_error_codes.py.-->
<int value="180" label="Growth Framework"/>
<int value="181" label="Audio Selection"/>
<int value="182" label="Extended Updates Available"/>
<int value="183" label="On-Device App Controls Available"/>
</enum>
<enum name="NQEObservationSource">