Skip to content

Commit

Permalink
PrivacyIndicators: Add launch settings notification button.
Browse files Browse the repository at this point in the history
Add the buttons to privacy indicators notification with the
functionality to launch the app settings. The button to launch the app
will be implemented in the future. Also, added unit tests for
PrivacyIndicatorsController.

Bug: 1344681
Change-Id: Ic97fe58da719fa37b57fb8268bf3ceef2d1877b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3827623
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Commit-Queue: Andre Le <leandre@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1035123}
  • Loading branch information
Andre Le authored and Chromium LUCI CQ committed Aug 15, 2022
1 parent e1b241c commit dc88516
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 14 deletions.
1 change: 1 addition & 0 deletions ash/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -2880,6 +2880,7 @@ test("ash_unittests") {
"system/power/power_prefs_unittest.cc",
"system/power/power_status_unittest.cc",
"system/power/video_activity_notifier_unittest.cc",
"system/privacy/privacy_indicators_controller_unittest.cc",
"system/privacy_hub/camera_privacy_switch_controller_unittest.cc",
"system/privacy_hub/microphone_privacy_switch_controller_unittest.cc",
"system/progress_indicator/progress_indicator_animation_registry_unittest.cc",
Expand Down
6 changes: 6 additions & 0 deletions ash/ash_strings.grd
Original file line number Diff line number Diff line change
Expand Up @@ -3930,6 +3930,12 @@ Here are some things you can try to get started.
<message name="IDS_PRIVACY_NOTIFICATION_MESSAGE_MIC" desc="Message label for a notification shown when an app is using the microphone.">
<ph name="APP_NAME">$1<ex>Google meet</ex></ph> is currently using your microphone
</message>
<message name="IDS_PRIVACY_NOTIFICATION_BUTTON_APP_LAUNCH" desc="Title for the launch app button of the notification shown when an app is using the camera/microphone.">
Go to app
</message>
<message name="IDS_PRIVACY_NOTIFICATION_BUTTON_APP_SETTINGS" desc="Title for the launch app button of the notification shown when an app is using the camera/microphone.">
App settings
</message>

<!-- Strings for microphone mute switch notification -->
<message name="IDS_MICROPHONE_MUTED_NOTIFICATION_TITLE" desc="Title for a notification shown to the users when an app tries to use the microphone while the microphone is muted.i Similar to IDS_MICROPHONE_MUTED_NOTIFICATION_TITLE_WITH_APP_NAME, except this message contains a generic app name string. Used when the name of the app that's using the microphone cannot be determined.">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b39e1039abbe8b4b283caaa49179df03e639d98b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0db2f5a5696cfaae437f53d87945927583da33df
43 changes: 41 additions & 2 deletions ash/system/privacy/privacy_indicators_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,46 @@
namespace ash {

namespace {

const char kPrivacyIndicatorsNotificationIdPrefix[] = "privacy-indicators";
const char kPrivacyIndicatorsNotifierId[] = "ash.privacy-indicators";

// Keep track of the button indexes in the privacy indicators notification.
enum PrivacyIndicatorsNotificationButton { kAppLaunch, kAppSettings };

} // namespace

PrivacyIndicatorsNotificationDelegate::PrivacyIndicatorsNotificationDelegate(
const AppActionClosure& launch_app,
const AppActionClosure& launch_settings)
: launch_app_(launch_app), launch_settings_(launch_settings) {}

PrivacyIndicatorsNotificationDelegate::
~PrivacyIndicatorsNotificationDelegate() = default;

void PrivacyIndicatorsNotificationDelegate::Click(
const absl::optional<int>& button_index,
const absl::optional<std::u16string>& reply) {
// Click on the notification body is no-op.
if (!button_index)
return;

switch (button_index.value()) {
case PrivacyIndicatorsNotificationButton::kAppLaunch:
launch_app_.Run();
break;
case PrivacyIndicatorsNotificationButton::kAppSettings:
launch_settings_.Run();
break;
}
}

void ModifyPrivacyIndicatorsNotification(
const std::string& app_id,
absl::optional<std::u16string> app_name,
bool camera_is_used,
bool microphone_is_used) {
bool microphone_is_used,
scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate) {
auto* message_center = message_center::MessageCenter::Get();
std::string id = kPrivacyIndicatorsNotificationIdPrefix + app_id;
bool notification_exist = message_center->FindVisibleNotificationById(id);
Expand All @@ -38,6 +69,7 @@ void ModifyPrivacyIndicatorsNotification(

std::u16string app_name_str = app_name.value_or(l10n_util::GetStringUTF16(
IDS_PRIVACY_NOTIFICATION_MESSAGE_DEFAULT_APP_NAME));

std::u16string title;
std::u16string message;
if (camera_is_used && microphone_is_used) {
Expand All @@ -60,6 +92,13 @@ void ModifyPrivacyIndicatorsNotification(
// Make the notification low priority so that it is silently added (no popup).
optional_fields.priority = message_center::LOW_PRIORITY;

// Note: The order of buttons added here should match the order in
// PrivacyIndicatorsNotificationButton.
optional_fields.buttons.emplace_back(
l10n_util::GetStringUTF16(IDS_PRIVACY_NOTIFICATION_BUTTON_APP_LAUNCH));
optional_fields.buttons.emplace_back(
l10n_util::GetStringUTF16(IDS_PRIVACY_NOTIFICATION_BUTTON_APP_SETTINGS));

auto notification = CreateSystemNotification(
message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE, id, title,
message,
Expand All @@ -69,7 +108,7 @@ void ModifyPrivacyIndicatorsNotification(
kPrivacyIndicatorsNotifierId,
NotificationCatalogName::kPrivacyIndicators),
optional_fields,
/*delegate=*/nullptr, kImeMenuMicrophoneIcon,
/*delegate=*/delegate, kImeMenuMicrophoneIcon,
message_center::SystemNotificationWarningLevel::NORMAL);

notification->set_accent_color_id(ui::kColorAshPrivacyIndicatorsBackground);
Expand Down
42 changes: 37 additions & 5 deletions ash/system/privacy/privacy_indicators_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,48 @@

#include "ash/ash_export.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/message_center/public/cpp/notification_delegate.h"

namespace ash {

using AppActionClosure = base::RepeatingCallback<void(void)>;

// An interface for the delegate of the privacy indicators notification,
// handling launching the app and its settings.
class ASH_EXPORT PrivacyIndicatorsNotificationDelegate
: public message_center::NotificationDelegate {
public:
PrivacyIndicatorsNotificationDelegate(
const AppActionClosure& launch_app,
const AppActionClosure& launch_settings);
PrivacyIndicatorsNotificationDelegate(
const PrivacyIndicatorsNotificationDelegate&) = delete;
PrivacyIndicatorsNotificationDelegate& operator=(
const PrivacyIndicatorsNotificationDelegate&) = delete;

// message_center::NotificationDelegate:
void Click(const absl::optional<int>& button_index,
const absl::optional<std::u16string>& reply) override;

protected:
~PrivacyIndicatorsNotificationDelegate() override;

private:
const AppActionClosure launch_app_;
const AppActionClosure launch_settings_;
};

// Add, update, or remove the privacy notification associated with the given
// `app_id`.
void ASH_EXPORT
ModifyPrivacyIndicatorsNotification(const std::string& app_id,
absl::optional<std::u16string> app_name,
bool camera_is_used,
bool microphone_is_used);
// The given scoped_refptr for `delegate` will be passed as a parameter for
// CreateSystemNotification() in case of adding/updating the notification, can
// be provided as a nullptr if irrelevant.
void ASH_EXPORT ModifyPrivacyIndicatorsNotification(
const std::string& app_id,
absl::optional<std::u16string> app_name,
bool camera_is_used,
bool microphone_is_used,
scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate);

} // namespace ash

Expand Down
135 changes: 135 additions & 0 deletions ash/system/privacy/privacy_indicators_controller_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/privacy/privacy_indicators_controller.h"

#include <string>

#include "ash/system/message_center/ash_message_popup_collection.h"
#include "ash/system/message_center/unified_message_center_bubble.h"
#include "ash/system/message_center/unified_message_center_view.h"
#include "ash/system/message_center/unified_message_list_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/ash_test_base.h"
#include "base/bind.h"
#include "ui/events/test/event_generator.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/views/notification_view_base.h"
#include "ui/views/widget/widget_utils.h"

namespace ash {

namespace {

const char kPrivacyIndicatorsNotificationIdPrefix[] = "privacy-indicators";

class TestDelegate : public PrivacyIndicatorsNotificationDelegate {
public:
TestDelegate()
: PrivacyIndicatorsNotificationDelegate(
base::BindRepeating(&TestDelegate::LaunchApp,
base::Unretained(this)),
base::BindRepeating(&TestDelegate::LaunchAppSettings,
base::Unretained(this))) {}
TestDelegate(const TestDelegate&) = delete;
TestDelegate& operator=(const TestDelegate&) = delete;

void LaunchApp() { launch_app_called_ = true; }
void LaunchAppSettings() { launch_settings_called_ = true; }

bool launch_app_called() { return launch_app_called_; }
bool launch_settings_called() { return launch_settings_called_; }

private:
~TestDelegate() override = default;

bool launch_app_called_ = false;
bool launch_settings_called_ = false;
};

} // namespace

class PrivacyIndicatorsControllerTest : public AshTestBase {
public:
PrivacyIndicatorsControllerTest() = default;
PrivacyIndicatorsControllerTest(const PrivacyIndicatorsControllerTest&) =
delete;
PrivacyIndicatorsControllerTest& operator=(
const PrivacyIndicatorsControllerTest&) = delete;
~PrivacyIndicatorsControllerTest() override = default;

// Get the notification view from message center associated with `id`.
views::View* GetNotificationViewFromMessageCenter(const std::string& id) {
return GetPrimaryUnifiedSystemTray()
->message_center_bubble()
->message_center_view()
->message_list_view()
->GetMessageViewForNotificationId(id);
}

// Get the popup notification view associated with `id`.
views::View* GetPopupNotificationView(const std::string& id) {
return GetPrimaryUnifiedSystemTray()
->GetMessagePopupCollection()
->GetMessageViewForNotificationId(id);
}

void ClickView(message_center::NotificationViewBase* view, int button_index) {
auto* action_buttons = view->GetViewByID(
message_center::NotificationViewBase::kActionButtonsRow);

auto* button_view = action_buttons->children()[button_index];

ui::test::EventGenerator generator(GetRootWindow(button_view->GetWidget()));
gfx::Point cursor_location = button_view->GetBoundsInScreen().CenterPoint();
generator.MoveMouseTo(cursor_location);
generator.ClickLeftButton();
}
};

TEST_F(PrivacyIndicatorsControllerTest, NotificationMetadata) {
std::string app_id = "test_app_id";
std::u16string app_name = u"test_app_name";
std::string notification_id = kPrivacyIndicatorsNotificationIdPrefix + app_id;
scoped_refptr<TestDelegate> delegate = base::MakeRefCounted<TestDelegate>();
ash::ModifyPrivacyIndicatorsNotification(
app_id, app_name, /*camera_is_used=*/true, /*microphone_is_used=*/true,
delegate);

auto* notification =
message_center::MessageCenter::Get()->FindNotificationById(
notification_id);

// Notification message should contains app name.
EXPECT_NE(std::string::npos, notification->message().find(app_name));
}

TEST_F(PrivacyIndicatorsControllerTest, NotificationClickButton) {
std::string app_id = "test_app_id";
std::string notification_id = kPrivacyIndicatorsNotificationIdPrefix + app_id;
scoped_refptr<TestDelegate> delegate = base::MakeRefCounted<TestDelegate>();
ash::ModifyPrivacyIndicatorsNotification(
app_id, u"test_app_name", /*camera_is_used=*/true,
/*microphone_is_used=*/true, delegate);

// Privacy indicators notification should not be a popup. It is silently added
// to the tray.
EXPECT_FALSE(GetPopupNotificationView(notification_id));
GetPrimaryUnifiedSystemTray()->ShowBubble();
auto* notification_view = static_cast<message_center::NotificationViewBase*>(
GetNotificationViewFromMessageCenter(notification_id));
EXPECT_TRUE(notification_view);

// Clicking the first button will trigger launching the app.
EXPECT_FALSE(delegate->launch_app_called());
ClickView(notification_view, 0);
EXPECT_TRUE(delegate->launch_app_called());

// Clicking the first button will trigger launching the app settings.
EXPECT_FALSE(delegate->launch_settings_called());
ClickView(notification_view, 1);
EXPECT_TRUE(delegate->launch_settings_called());
}

} // namespace ash
28 changes: 21 additions & 7 deletions chrome/browser/ui/ash/app_access_notifier.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/account_id/account_id.h"
Expand All @@ -36,13 +35,10 @@ apps::AppCapabilityAccessCache* GetAppCapabilityAccessCache(
}

apps::AppRegistryCache* GetActiveUserAppRegistryCache() {
auto* manager = user_manager::UserManager::Get();
const user_manager::User* active_user = manager->GetActiveUser();
if (!active_user)
Profile* profile = ProfileManager::GetActiveUserProfile();
if (!profile)
return nullptr;

auto account_id = active_user->GetAccountId();
Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(active_user);
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
return &proxy->AppRegistryCache();
Expand Down Expand Up @@ -70,6 +66,19 @@ absl::optional<std::u16string> MapAppIdToShortName(
return absl::nullopt;
}

void LaunchApp(const std::string& app_id) {
// TODO(crbug/1351250): Finish this function.
}

// Launch the native settings page of the app with `app_id`.
void LaunchAppSettings(const std::string& app_id) {
Profile* profile = ProfileManager::GetActiveUserProfile();
if (!profile)
return;
apps::AppServiceProxyFactory::GetForProfile(profile)->OpenNativeSettings(
app_id);
}

} // namespace

AppAccessNotifier::AppAccessNotifier() {
Expand Down Expand Up @@ -109,10 +118,15 @@ void AppAccessNotifier::OnCapabilityAccessUpdate(

if (ash::features::IsPrivacyIndicatorsEnabled()) {
auto app_id = update.AppId();

auto launch_app = base::BindRepeating(&LaunchApp, app_id);
auto launch_settings = base::BindRepeating(&LaunchAppSettings, app_id);
ash::ModifyPrivacyIndicatorsNotification(
app_id,
GetAppShortNameFromAppId(app_id, GetActiveUserAppRegistryCache()),
camera_is_used, microphone_is_used);
camera_is_used, microphone_is_used,
base::MakeRefCounted<ash::PrivacyIndicatorsNotificationDelegate>(
launch_app, launch_settings));
}

if (microphone_is_used) {
Expand Down

0 comments on commit dc88516

Please sign in to comment.