Commit 2edfabbd authored by Jason Lin's avatar Jason Lin Committed by Commit Bot

Implement VM Mic/Camera indicators on systray

The implementation is based on UX design [1].

[1] https://docs.google.com/presentation/d/1MWWejK-y2vnBN3Wg__Kd_LIiAcIF3VctzMowMI3wM4w/edit#slide=id.g91c6bdd2e4_0_12

Bug: b/167491603
Change-Id: Id107ea7b4b90aad0714a617147acc877adb9fc1d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2397979
Commit-Queue: Jason Lin <lxj@google.com>
Reviewed-by: default avatarTim Song <tengs@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarJoel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#814566}
parent 4c91ed5b
......@@ -1297,6 +1297,8 @@ component("ash") {
"system/tray/unfocusable_label.cc",
"system/tray/unfocusable_label.h",
"system/tray/view_click_listener.h",
"system/unified/camera_mic_tray_item_view.cc",
"system/unified/camera_mic_tray_item_view.h",
"system/unified/collapse_button.cc",
"system/unified/collapse_button.h",
"system/unified/current_locale_view.cc",
......@@ -2175,6 +2177,7 @@ test("ash_unittests") {
"system/tray/tray_event_filter_unittest.cc",
"system/tray/tray_info_label_unittest.cc",
"system/tray/tri_view_unittest.cc",
"system/unified/camera_mic_tray_item_view_unittest.cc",
"system/unified/feature_pods_container_view_unittest.cc",
"system/unified/notification_counter_view_unittest.cc",
"system/unified/page_indicator_view_unittest.cc",
......
......@@ -190,13 +190,15 @@ This file contains the strings for ash.
</message>
<!-- Status tray items -->
<message name="IDS_ASH_STATUS_TRAY_ACCESSIBLE_DESCRIPTION" desc="The accessible description of the status tray and the information on it.">
<message name="IDS_ASH_STATUS_TRAY_ACCESSIBLE_DESCRIPTION" is_accessibility_with_no_ui="true" desc="The accessible description of the status tray and the information on it.">
Status tray, time <ph name="time">$1<ex>9:50</ex></ph>,
<ph name="battery">$2<ex>Battery is full.</ex></ph>
<ph name="network">$3<ex>Connected to Wifi.</ex></ph>,
<ph name="notification">$4<ex>1 notification</ex></ph>,
<ph name="ime">$5<ex>Using US keyboard</ex></ph>
<ph name="locale">$6<ex>Using English</ex></ph>
<ph name="mic">$4<ex>A virtual machine is using your microphone</ex></ph>,
<ph name="camera">$5<ex>A virtual machine is using your camera</ex></ph>,
<ph name="notification">$6<ex>1 notification</ex></ph>,
<ph name="ime">$7<ex>Using US keyboard</ex></ph>
<ph name="locale">$8<ex>Using English</ex></ph>
</message>
<message name="IDS_ASH_QUICK_SETTINGS_BUBBLE_ACCESSIBLE_DESCRIPTION" desc="The accessible description of the quick settings bubble and the information in it.">
Quick Settings, Press search + left to access the notification center.
......@@ -2805,6 +2807,14 @@ This file contains the strings for ash.
Here are some things you can try to get started.
</message>
<!-- For CameraMicTrayItemView -->
<message name="IDS_ASH_CAMERA_MIC_VM_USING_CAMERA" desc="Tooltip message shown at the systray camera indicator when a VM is using the camera">
A virtual machine is using your camera
</message>
<message name="IDS_ASH_CAMERA_MIC_VM_USING_MIC" desc="Tooltip message shown at the systray microphone indicator when a VM is using the microphone">
A virtual machine is using your microphone
</message>
<message name="IDS_ASH_MESSAGE_CENTER_UNLOCK_TO_PERFORM_ACTION" desc="The short message to encourage user to unlock the device so that Chrome OS can perform the notification action selected by user after unlocking.">
Unlock device to perform the notification action
</message>
......
d591db48d6c57816890d85213d98f10f6c322a6b
\ No newline at end of file
22591a040bd7307c93d9a3c760ed8c976a0b6e22
\ No newline at end of file
......@@ -93,6 +93,12 @@ void MediaControllerImpl::NotifyCaptureState(
observer.OnMediaCaptureChanged(capture_states);
}
void MediaControllerImpl::NotifyVmCaptureState(
MediaCaptureState capture_state) {
for (auto& observer : observers_)
observer.OnVmMediaCaptureChanged(capture_state);
}
void MediaControllerImpl::HandleMediaPlayPause() {
if (Shell::Get()->session_controller()->IsScreenLocked() &&
!AreLockScreenMediaKeysEnabled()) {
......
......@@ -25,7 +25,9 @@ class MediaCaptureObserver {
public:
// Called when media capture state has changed.
virtual void OnMediaCaptureChanged(
const base::flat_map<AccountId, MediaCaptureState>& capture_states) = 0;
const base::flat_map<AccountId, MediaCaptureState>& capture_states) {}
// Called when a VM's media capture state has changed.
virtual void OnVmMediaCaptureChanged(MediaCaptureState state) {}
protected:
virtual ~MediaCaptureObserver() {}
......@@ -55,6 +57,7 @@ class ASH_EXPORT MediaControllerImpl
void SetForceMediaClientKeyHandling(bool enabled) override;
void NotifyCaptureState(const base::flat_map<AccountId, MediaCaptureState>&
capture_states) override;
void NotifyVmCaptureState(MediaCaptureState capture_state) override;
// If media session accelerators are enabled then these methods will use the
// media session service to control playback. Otherwise it will forward to
......
......@@ -41,6 +41,13 @@ class ASH_PUBLIC_EXPORT MediaController {
// MediaCaptureState representing every user's state.
virtual void NotifyCaptureState(
const base::flat_map<AccountId, MediaCaptureState>& capture_states) = 0;
// Called when a VM's media capture state changes. There is no `AccountId` in
// the argument because only the primary account/profile can launch a VM.
//
// TODO(b/167491603): We should consider merging this with
// `NotifyCaptureState()` if the browser also uses the same systray capturing
// indicators as VMs'.
virtual void NotifyVmCaptureState(MediaCaptureState capture_state) = 0;
protected:
MediaController();
......
// Copyright 2020 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/unified/camera_mic_tray_item_view.h"
#include <algorithm>
#include "ash/public/cpp/media_controller.h"
#include "ash/public/cpp/vm_camera_mic_constants.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/tray_constants.h"
#include "base/feature_list.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/message_center/message_center.h"
#include "ui/views/controls/image_view.h"
namespace ash {
CameraMicTrayItemView::CameraMicTrayItemView(Shelf* shelf, Type type)
: TrayItemView(shelf), type_(type) {
CreateImageView();
FetchMessage();
const gfx::VectorIcon* icon = nullptr;
switch (type_) {
case Type::kCamera:
icon = &::vector_icons::kVideocamIcon;
break;
case Type::kMic:
icon = &::vector_icons::kMicIcon;
break;
}
image_view()->SetImage(gfx::CreateVectorIcon(gfx::IconDescription(
*icon, kUnifiedTrayIconSize,
AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary))));
auto* shell = Shell::Get();
shell->session_controller()->AddObserver(this);
shell->media_controller()->AddObserver(this);
SetVisible(false);
}
CameraMicTrayItemView::~CameraMicTrayItemView() {
auto* shell = Shell::Get();
shell->media_controller()->RemoveObserver(this);
shell->session_controller()->RemoveObserver(this);
}
void CameraMicTrayItemView::OnVmMediaCaptureChanged(
MediaCaptureState capture_state) {
switch (type_) {
case Type::kCamera:
active_ = (capture_state == MediaCaptureState::kVideo ||
capture_state == MediaCaptureState::kAudioVideo);
break;
case Type::kMic:
active_ = (capture_state == MediaCaptureState::kAudio ||
capture_state == MediaCaptureState::kAudioVideo);
break;
}
Update();
}
const char* CameraMicTrayItemView::GetClassName() const {
return "CameraMicTrayItemView";
}
void CameraMicTrayItemView::Update() {
// Hide for non-primary session because we only show the indicators for VMs
// for now, and VMs support only the primary session.
SetVisible(active_ && is_primary_session_ &&
base::FeatureList::IsEnabled(
chromeos::features::kVmCameraMicIndicatorsAndNotifications));
}
base::string16 CameraMicTrayItemView::GetAccessibleNameString() const {
return message_;
}
views::View* CameraMicTrayItemView::GetTooltipHandlerForPoint(
const gfx::Point& point) {
return GetLocalBounds().Contains(point) ? this : nullptr;
}
base::string16 CameraMicTrayItemView::GetTooltipText(
const gfx::Point& p) const {
return message_;
}
void CameraMicTrayItemView::OnActiveUserSessionChanged(
const AccountId& account_id) {
is_primary_session_ = Shell::Get()->session_controller()->IsUserPrimary();
Update();
}
void CameraMicTrayItemView::HandleLocaleChange() {
FetchMessage();
}
void CameraMicTrayItemView::FetchMessage() {
switch (type_) {
case Type::kCamera:
message_ = l10n_util::GetStringUTF16(IDS_ASH_CAMERA_MIC_VM_USING_CAMERA);
break;
case Type::kMic:
message_ = l10n_util::GetStringUTF16(IDS_ASH_CAMERA_MIC_VM_USING_MIC);
break;
}
}
} // namespace ash
// Copyright 2020 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.
#ifndef ASH_SYSTEM_UNIFIED_CAMERA_MIC_TRAY_ITEM_VIEW_H_
#define ASH_SYSTEM_UNIFIED_CAMERA_MIC_TRAY_ITEM_VIEW_H_
#include "ash/media/media_controller_impl.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/system/tray/tray_item_view.h"
#include "base/macros.h"
namespace ash {
// An indicator shown in UnifiedSystemTray when a VM is accessing the camera or
// mic. We might want to extend this feature to the browser in the future.
class ASH_EXPORT CameraMicTrayItemView : public TrayItemView,
public SessionObserver,
public MediaCaptureObserver {
public:
enum class Type {
kCamera,
kMic,
};
CameraMicTrayItemView(Shelf* shelf, Type type);
~CameraMicTrayItemView() override;
CameraMicTrayItemView(const CameraMicTrayItemView&) = delete;
CameraMicTrayItemView& operator=(const CameraMicTrayItemView&) = delete;
base::string16 GetAccessibleNameString() const;
// views::View:
const char* GetClassName() const override;
views::View* GetTooltipHandlerForPoint(const gfx::Point& point) override;
base::string16 GetTooltipText(const gfx::Point& p) const override;
// SessionObserver:
void OnActiveUserSessionChanged(const AccountId& account_id) override;
// TrayItemView:
void HandleLocaleChange() override;
// MediaCaptureObserver:
void OnVmMediaCaptureChanged(MediaCaptureState capture_state) override;
private:
void Update();
void FetchMessage();
const Type type_;
bool active_ = false;
bool is_primary_session_ = false;
base::string16 message_;
};
} // namespace ash
#endif // ASH_SYSTEM_UNIFIED_CAMERA_MIC_TRAY_ITEM_VIEW_H_
// Copyright 2020 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/unified/camera_mic_tray_item_view.h"
#include <memory>
#include <utility>
#include "ash/public/cpp/media_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
using Type = CameraMicTrayItemView::Type;
MediaCaptureState GetRelevantCaptureState(Type type) {
switch (type) {
case Type::kCamera:
return MediaCaptureState::kVideo;
case Type::kMic:
return MediaCaptureState::kAudio;
}
}
MediaCaptureState GetIrrelevantCaptureState(Type type) {
switch (type) {
case Type::kCamera:
return MediaCaptureState::kAudio;
case Type::kMic:
return MediaCaptureState::kVideo;
}
}
} // namespace
class CameraMicTrayItemViewTest : public AshTestBase,
public testing::WithParamInterface<Type> {
public:
// AshTestBase:
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(
chromeos::features::kVmCameraMicIndicatorsAndNotifications);
AshTestBase::SetUp();
camera_mic_tray_item_view_ =
std::make_unique<CameraMicTrayItemView>(GetPrimaryShelf(), GetParam());
// Relogin to make sure `OnActiveUserSessionChanged` is triggered.
ClearLogin();
SimulateUserLogin("user@test.com");
}
void TearDown() override {
camera_mic_tray_item_view_.reset();
AshTestBase::TearDown();
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<CameraMicTrayItemView> camera_mic_tray_item_view_;
};
TEST_P(CameraMicTrayItemViewTest, OnVmMediaCaptureChanged) {
Type type = GetParam();
MediaCaptureState relevant = GetRelevantCaptureState(type);
MediaCaptureState irrelevant = GetIrrelevantCaptureState(type);
EXPECT_FALSE(camera_mic_tray_item_view_->GetVisible());
camera_mic_tray_item_view_->OnVmMediaCaptureChanged(relevant);
EXPECT_TRUE(camera_mic_tray_item_view_->GetVisible());
camera_mic_tray_item_view_->OnVmMediaCaptureChanged(irrelevant);
EXPECT_FALSE(camera_mic_tray_item_view_->GetVisible());
camera_mic_tray_item_view_->OnVmMediaCaptureChanged(
static_cast<MediaCaptureState>(static_cast<int>(relevant) |
static_cast<int>(irrelevant)));
EXPECT_TRUE(camera_mic_tray_item_view_->GetVisible());
camera_mic_tray_item_view_->OnVmMediaCaptureChanged(MediaCaptureState::kNone);
EXPECT_FALSE(camera_mic_tray_item_view_->GetVisible());
}
TEST_P(CameraMicTrayItemViewTest, HideForNonPrimaryUser) {
camera_mic_tray_item_view_->OnVmMediaCaptureChanged(
GetRelevantCaptureState(GetParam()));
EXPECT_TRUE(camera_mic_tray_item_view_->GetVisible());
SimulateUserLogin("user2@test.com");
EXPECT_FALSE(camera_mic_tray_item_view_->GetVisible());
}
INSTANTIATE_TEST_SUITE_P(All,
CameraMicTrayItemViewTest,
testing::Values(Type::kCamera, Type::kMic));
} // namespace ash
......@@ -25,6 +25,7 @@
#include "ash/system/time/time_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_container.h"
#include "ash/system/unified/camera_mic_tray_item_view.h"
#include "ash/system/unified/current_locale_view.h"
#include "ash/system/unified/ime_mode_view.h"
#include "ash/system/unified/managed_device_tray_item_view.h"
......@@ -128,6 +129,11 @@ UnifiedSystemTray::UnifiedSystemTray(Shelf* shelf)
current_locale_view_(new CurrentLocaleView(shelf)),
ime_mode_view_(new ImeModeView(shelf)),
managed_device_view_(new ManagedDeviceTrayItemView(shelf)),
camera_view_(
new CameraMicTrayItemView(shelf,
CameraMicTrayItemView::Type::kCamera)),
mic_view_(
new CameraMicTrayItemView(shelf, CameraMicTrayItemView::Type::kMic)),
notification_counter_item_(new NotificationCounterView(shelf)),
quiet_mode_view_(new QuietModeView(shelf)),
time_view_(new tray::TimeTrayItemView(shelf)) {
......@@ -140,6 +146,8 @@ UnifiedSystemTray::UnifiedSystemTray(Shelf* shelf)
AddTrayItemToContainer(managed_device_view_);
AddTrayItemToContainer(notification_counter_item_);
AddTrayItemToContainer(quiet_mode_view_);
AddTrayItemToContainer(camera_view_);
AddTrayItemToContainer(mic_view_);
if (features::IsSeparateNetworkIconsEnabled()) {
network_tray_view_ =
......@@ -400,6 +408,12 @@ base::string16 UnifiedSystemTray::GetAccessibleNameForTray() {
status.push_back(network_tray_view_->GetVisible()
? network_tray_view_->GetAccessibleNameString()
: base::EmptyString16());
status.push_back(mic_view_->GetVisible()
? mic_view_->GetAccessibleNameString()
: base::EmptyString16());
status.push_back(camera_view_->GetVisible()
? camera_view_->GetAccessibleNameString()
: base::EmptyString16());
status.push_back(notification_counter_item_->GetVisible()
? notification_counter_item_->GetAccessibleNameString()
: base::EmptyString16());
......
......@@ -36,6 +36,7 @@ class UnifiedSliderBubbleController;
class UnifiedSystemTrayBubble;
class UnifiedSystemTrayModel;
class UnifiedMessageCenterBubble;
class CameraMicTrayItemView;
// The UnifiedSystemTray is the system menu of Chromium OS, which is a clickable
// rounded rectangle typically located on the bottom right corner of the screen,
......@@ -206,6 +207,8 @@ class ASH_EXPORT UnifiedSystemTray : public TrayBackgroundView,
CurrentLocaleView* const current_locale_view_;
ImeModeView* const ime_mode_view_;
ManagedDeviceTrayItemView* const managed_device_view_;
CameraMicTrayItemView* const camera_view_;
CameraMicTrayItemView* const mic_view_;
NotificationCounterView* const notification_counter_item_;
QuietModeView* const quiet_mode_view_;
tray::TimeTrayItemView* const time_view_;
......
......@@ -21,10 +21,10 @@ class Profile;
namespace chromeos {
// This class manages camera/mic access (and the access notifications) for VMs
// (crostini and parallels for now). It is only available for the primary
// profile since all VMs do not support non-primary profiles anyway. We might
// need to change this if we extend this class to support the browser (and we
// will need to make the notification ids different for different profiles).
// (crostini and parallels for now). Like all the VMs, it is only available for
// the primary and non-incognito profile. We might need to change this if we
// extend this class to support the browser, in which case we will also need to
// make the notification ids different for different profiles.
class VmCameraMicManager : public KeyedService {
public:
enum class VmType { kCrostiniVm, kPluginVm };
......@@ -39,6 +39,7 @@ class VmCameraMicManager : public KeyedService {
virtual void OnVmCameraMicActiveChanged(VmCameraMicManager*) {}
};
// Return nullptr if the profile is non-primary or incognito.
static VmCameraMicManager* GetForProfile(Profile* profile);
explicit VmCameraMicManager(Profile* profile);
......
......@@ -13,6 +13,7 @@
#include "base/single_thread_task_runner.h"
#include "base/task/current_thread.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/chromeos/camera_mic/vm_camera_mic_manager.h"
#include "chrome/browser/chromeos/extensions/media_player_api.h"
#include "chrome/browser/chromeos/extensions/media_player_event_router.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
......@@ -139,6 +140,11 @@ MediaClientImpl::MediaClientImpl() {
MediaCaptureDevicesDispatcher::GetInstance()->AddObserver(this);
BrowserList::AddObserver(this);
auto* vm_camera_mic_manager = chromeos::VmCameraMicManager::GetForProfile(
ProfileManager::GetPrimaryUserProfile());
if (vm_camera_mic_manager)
vm_camera_mic_manager->AddObserver(this);
DCHECK(!g_media_client);
g_media_client = this;
}
......@@ -151,6 +157,11 @@ MediaClientImpl::~MediaClientImpl() {
MediaCaptureDevicesDispatcher::GetInstance()->RemoveObserver(this);
BrowserList::RemoveObserver(this);
auto* vm_camera_mic_manager = chromeos::VmCameraMicManager::GetForProfile(
ProfileManager::GetPrimaryUserProfile());
if (vm_camera_mic_manager)
vm_camera_mic_manager->RemoveObserver(this);
}
// static
......@@ -240,6 +251,17 @@ void MediaClientImpl::OnBrowserSetLastActive(Browser* browser) {
UpdateForceMediaClientKeyHandling();
}
void MediaClientImpl::OnVmCameraMicActiveChanged(
chromeos::VmCameraMicManager* manager) {
using DeviceType = chromeos::VmCameraMicManager::DeviceType;
MediaCaptureState state = MediaCaptureState::kNone;
if (manager->GetDeviceActive(DeviceType::kCamera))
state |= MediaCaptureState::kVideo;
if (manager->GetDeviceActive(DeviceType::kMic))
state |= MediaCaptureState::kAudio;
media_controller_->NotifyVmCaptureState(state);
}
void MediaClientImpl::EnableCustomMediaKeyHandler(
content::BrowserContext* context,
ui::MediaKeysListener::Delegate* delegate) {
......
......@@ -9,6 +9,7 @@
#include "base/containers/flat_map.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/chromeos/camera_mic/vm_camera_mic_manager.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "ui/base/accelerators/media_keys_listener.h"
......@@ -20,6 +21,7 @@ class MediaController;
class MediaClientImpl : public ash::MediaClient,
public BrowserListObserver,
public chromeos::VmCameraMicManager::Observer,
public MediaCaptureDevicesDispatcher::Observer {
public:
MediaClientImpl();
......@@ -55,6 +57,10 @@ class MediaClientImpl : public ash::MediaClient,
// BrowserListObserver:
void OnBrowserSetLastActive(Browser* browser) override;
// chromeos::VmCameraMicManager::Observer
void OnVmCameraMicActiveChanged(
chromeos::VmCameraMicManager* manager) override;
// Enables/disables custom media key handling when |context| is the active
// browser. Media keys will be forwarded to |delegate|.
void EnableCustomMediaKeyHandler(content::BrowserContext* context,
......
......@@ -30,6 +30,8 @@ class TestMediaController : public ash::MediaController {
const base::flat_map<AccountId, ash::MediaCaptureState>& capture_states)
override {}
void NotifyVmCaptureState(ash::MediaCaptureState capture_states) override {}
bool force_media_client_key_handling() const {
return force_media_client_key_handling_;
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment