// Copyright 2022 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/system/privacy/privacy_indicators_tray_item_view.h"
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/notification_center_tray.h"
#include "ash/system/privacy/privacy_indicators_controller.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/scoped_refptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
namespace {
const int kPrivacyIndicatorsViewExpandedShorterSideSize = 24;
const int kPrivacyIndicatorsViewExpandedLongerSideSize = 50;
const int kPrivacyIndicatorsViewSize = 8;
constexpr char kPrivacyIndicatorsShowTypeHistogramName[] =
"Ash.PrivacyIndicators.ShowType";
constexpr char kPrivacyIndicatorsShowPerSessionHistogramName[] =
"Ash.PrivacyIndicators.NumberOfShowsPerSession";
constexpr char kCountAppsAccessCameraHistogramName[] =
"Ash.PrivacyIndicators.NumberOfAppsAccessingCamera";
constexpr char kCountAppsAccessMicrophoneHistogramName[] =
"Ash.PrivacyIndicators.NumberOfAppsAccessingMicrophone";
constexpr char kRepeatedShowsHistogramName[] =
"Ash.PrivacyIndicators.NumberOfRepeatedShows";
constexpr char kVisibilityDurationHistogramName[] =
"Ash.PrivacyIndicators.IndicatorShowsDuration";
// Update the state of accessing camera and microphone using the
// `PrivacyIndicatorsController`.
void UpdateCameraAndMicrophoneUsage(bool is_camera_used,
bool is_microphone_used,
const std::string& app_id = "app_id") {
ash::PrivacyIndicatorsController::Get()->UpdatePrivacyIndicators(
app_id, /*app_name=*/u"App Name", is_camera_used, is_microphone_used,
base::MakeRefCounted<ash::PrivacyIndicatorsNotificationDelegate>(),
ash::PrivacyIndicatorsSource::kApps);
}
// Get the expected size in expand animation, given the animation value.
int GetExpectedSizeInExpandAnimation(double progress) {
return kPrivacyIndicatorsViewExpandedLongerSideSize *
gfx::Tween::CalculateValue(gfx::Tween::ACCEL_20_DECEL_100, progress);
}
// Get the expected size in shrink animation, given the animation value.
int GetExpectedSizeInShrinkAnimation(bool for_longer_side, double progress) {
double animation_value =
gfx::Tween::CalculateValue(gfx::Tween::ACCEL_20_DECEL_100, progress);
int begin_size = for_longer_side
? kPrivacyIndicatorsViewExpandedLongerSideSize
: kPrivacyIndicatorsViewExpandedShorterSideSize;
return begin_size -
(begin_size - kPrivacyIndicatorsViewSize) * animation_value;
}
// Get the expected tooltip text, given the string for camera/mic access and
// screen share.
std::u16string GetExpectedTooltipText(std::u16string cam_mic_status,
std::u16string screen_share_status) {
if (cam_mic_status.empty()) {
return screen_share_status;
}
if (screen_share_status.empty()) {
return cam_mic_status;
}
return l10n_util::GetStringFUTF16(IDS_PRIVACY_INDICATORS_VIEW_TOOLTIP,
{cam_mic_status, screen_share_status},
/*offsets=*/nullptr);
}
} // namespace
namespace ash {
class PrivacyIndicatorsTrayItemViewTest
: public AshTestBase,
public testing::WithParamInterface<bool> {
public:
PrivacyIndicatorsTrayItemViewTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
PrivacyIndicatorsTrayItemViewTest(const PrivacyIndicatorsTrayItemViewTest&) =
delete;
PrivacyIndicatorsTrayItemViewTest& operator=(
const PrivacyIndicatorsTrayItemViewTest&) = delete;
~PrivacyIndicatorsTrayItemViewTest() override = default;
std::u16string GetTooltipText() {
return privacy_indicators_view()->GetTooltipText(gfx::Point());
}
views::BoxLayout* GetLayoutManager(
PrivacyIndicatorsTrayItemView* privacy_indicators_view) {
return privacy_indicators_view->layout_manager_;
}
void AnimateToValue(gfx::LinearAnimation* animation, double animation_value) {
EXPECT_TRUE(animation->is_animating());
animation->SetCurrentValue(animation_value);
privacy_indicators_view()->AnimationProgressed(animation);
}
// Set `privacy_indicators_view()` to be visible and perform animation.
void SetViewVisibleWithAnimation() {
privacy_indicators_view()->SetVisible(true);
privacy_indicators_view()->PerformAnimation();
}
// Simulates completing the animation.
void SimulateAnimationEnded() {
privacy_indicators_view()->AnimationEnded(
privacy_indicators_view()->shorter_side_shrink_animation_.get());
}
protected:
PrivacyIndicatorsTrayItemView* privacy_indicators_view() const {
return Shell::GetPrimaryRootWindowController()
->GetStatusAreaWidget()
->notification_center_tray()
->privacy_indicators_view();
}
PrivacyIndicatorsTrayItemView* GetSecondaryDisplayPrivacyIndicatorsView()
const {
auto* status_area_widget =
Shell::GetRootWindowControllerWithDisplayId(GetSecondaryDisplay().id())
->GetStatusAreaWidget();
return status_area_widget->notification_center_tray()
->privacy_indicators_view();
}
views::ImageView* camera_icon() {
return privacy_indicators_view()->camera_icon_;
}
views::ImageView* microphone_icon() {
return privacy_indicators_view()->microphone_icon_;
}
views::ImageView* screen_share_icon() {
return privacy_indicators_view()->screen_share_icon_;
}
gfx::LinearAnimation* expand_animation() {
return privacy_indicators_view()->expand_animation_.get();
}
PrivacyIndicatorsTrayItemView::AnimationState animation_state() {
return privacy_indicators_view()->animation_state_;
}
gfx::LinearAnimation* longer_side_shrink_animation() {
return privacy_indicators_view()->longer_side_shrink_animation_.get();
}
gfx::LinearAnimation* shorter_side_shrink_animation() {
return privacy_indicators_view()->shorter_side_shrink_animation_.get();
}
};
TEST_F(PrivacyIndicatorsTrayItemViewTest, IconsVisibility) {
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/true);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, IconsVisibilityAfterAnimation) {
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kExpand,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
// No icons shown after the animation.
SimulateAnimationEnded();
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
// Since there's no new sensor and new media stream added, no icons should be
// visible and animation should not be triggered.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
// New sensor is accessed (microphone), so we show all icons accessing that
// particular app. Animation should start.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kExpand,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
SimulateAnimationEnded();
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
// New app accessed, show the indicator according to that app.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/true, /*app_id=*/"app_id2");
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kExpand,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
SimulateAnimationEnded();
// Updates the previous app. However, since no new sensor is accessed
// (microphone is already accessed by the second app), the indicator should
// remain the same with no animation.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, ScreenShareIconsVisibility) {
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/true);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(screen_share_icon()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/false);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
// Test screen share showing up with other icons.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/true);
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/true);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
EXPECT_TRUE(screen_share_icon()->GetVisible());
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/false);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
EXPECT_FALSE(screen_share_icon()->GetVisible());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, TooltipText) {
EXPECT_EQ(GetExpectedTooltipText(/*cam_mic_status=*/std::u16string(),
/*screen_share_status=*/std::u16string()),
GetTooltipText());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
EXPECT_EQ(GetExpectedTooltipText(/*cam_mic_status=*/l10n_util::GetStringUTF16(
IDS_PRIVACY_INDICATORS_STATUS_CAMERA),
/*screen_share_status=*/std::u16string()),
GetTooltipText());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/true);
EXPECT_EQ(GetExpectedTooltipText(/*cam_mic_status=*/l10n_util::GetStringUTF16(
IDS_PRIVACY_INDICATORS_STATUS_MIC),
/*screen_share_status=*/std::u16string()),
GetTooltipText());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
EXPECT_EQ(
GetExpectedTooltipText(/*cam_mic_status=*/l10n_util::GetStringUTF16(
IDS_PRIVACY_INDICATORS_STATUS_CAMERA_AND_MIC),
/*screen_share_status=*/std::u16string()),
GetTooltipText());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
EXPECT_EQ(GetExpectedTooltipText(/*cam_mic_status=*/std::u16string(),
/*screen_share_status=*/std::u16string()),
GetTooltipText());
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/true);
EXPECT_EQ(GetExpectedTooltipText(
/*cam_mic_status=*/std::u16string(),
/*screen_share_status=*/l10n_util::GetStringUTF16(
IDS_ASH_STATUS_TRAY_SCREEN_SHARE_TITLE)),
GetTooltipText());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, ShelfAlignmentChanged) {
auto* view = privacy_indicators_view();
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kLeft);
EXPECT_EQ(views::BoxLayout::Orientation::kVertical,
GetLayoutManager(view)->GetOrientation());
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kBottom);
EXPECT_EQ(views::BoxLayout::Orientation::kHorizontal,
GetLayoutManager(view)->GetOrientation());
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kRight);
EXPECT_EQ(views::BoxLayout::Orientation::kVertical,
GetLayoutManager(view)->GetOrientation());
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kBottomLocked);
EXPECT_EQ(views::BoxLayout::Orientation::kHorizontal,
GetLayoutManager(view)->GetOrientation());
}
// Tests that the privacy indicators tray item is visible when its show
// animation finishes running after the notification center tray has been
// hidden. This test was added in response to b/283091001.
TEST_F(PrivacyIndicatorsTrayItemViewTest,
ShowAnimationAfterNotificationCenterTrayHidden) {
// Verify that the privacy indicators are hidden and not animating.
ASSERT_FALSE(privacy_indicators_view()->GetVisible());
ASSERT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
// Show the notification center tray.
GetPrimaryNotificationCenterTray()->SetVisiblePreferred(true);
ASSERT_TRUE(GetPrimaryNotificationCenterTray()->IsDrawn());
ASSERT_EQ(GetPrimaryNotificationCenterTray()->layer()->opacity(), 1.0f);
// Hide the notification center tray.
GetPrimaryNotificationCenterTray()->SetVisiblePreferred(false);
ASSERT_FALSE(GetPrimaryNotificationCenterTray()->IsDrawn());
ASSERT_EQ(GetPrimaryNotificationCenterTray()->layer()->opacity(), 0.0f);
// Show privacy indicators and let the animation end.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true);
SimulateAnimationEnded();
ASSERT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
// Verify that the privacy indicators tray item is visible.
EXPECT_TRUE(privacy_indicators_view()->IsDrawn());
EXPECT_EQ(privacy_indicators_view()->layer()->opacity(), 1.0f);
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, VisibilityAnimation) {
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kBottom);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
SetViewVisibleWithAnimation();
double progress = 0.5;
// Firstly, expand animation will be performed.
expand_animation()->Start();
AnimateToValue(expand_animation(), progress);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kExpand,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().height());
EXPECT_EQ(GetExpectedSizeInExpandAnimation(progress),
privacy_indicators_view()->GetPreferredSize().width());
expand_animation()->End();
// When expand animation ends, the view will be in `kDwellInExpand` state.
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kDwellInExpand,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().height());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedLongerSideSize,
privacy_indicators_view()->GetPreferredSize().width());
// After that shrink animations will be started.
longer_side_shrink_animation()->Start();
AnimateToValue(longer_side_shrink_animation(), progress);
EXPECT_EQ(
PrivacyIndicatorsTrayItemView::AnimationState::kOnlyLongerSideShrink,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().height());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/true, progress),
privacy_indicators_view()->GetPreferredSize().width());
shorter_side_shrink_animation()->Start();
AnimateToValue(shorter_side_shrink_animation(), progress);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kBothSideShrink,
animation_state());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/false, progress),
privacy_indicators_view()->GetPreferredSize().height());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/true, progress),
privacy_indicators_view()->GetPreferredSize().width());
longer_side_shrink_animation()->End();
shorter_side_shrink_animation()->End();
// When finish, the view should have the size of a dot.
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewSize,
privacy_indicators_view()->GetPreferredSize().height());
EXPECT_EQ(kPrivacyIndicatorsViewSize,
privacy_indicators_view()->GetPreferredSize().width());
// All icon should not be visible.
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
EXPECT_FALSE(screen_share_icon()->GetVisible());
}
// Same test as above, but with the side shelf (the longer and shorter side will
// be flipped).
TEST_F(PrivacyIndicatorsTrayItemViewTest, SideShelfVisibilityAnimation) {
GetPrimaryShelf()->SetAlignment(ShelfAlignment::kLeft);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
SetViewVisibleWithAnimation();
double progress = 0.5;
// Firstly, expand animation will be performed.
expand_animation()->Start();
AnimateToValue(expand_animation(), progress);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kExpand,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().width());
EXPECT_EQ(GetExpectedSizeInExpandAnimation(progress),
privacy_indicators_view()->GetPreferredSize().height());
expand_animation()->End();
// When expand animation ends, the view will be in `kDwellInExpand` state.
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kDwellInExpand,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().width());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedLongerSideSize,
privacy_indicators_view()->GetPreferredSize().height());
// After that shrink animations will be started.
longer_side_shrink_animation()->Start();
AnimateToValue(longer_side_shrink_animation(), progress);
EXPECT_EQ(
PrivacyIndicatorsTrayItemView::AnimationState::kOnlyLongerSideShrink,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewExpandedShorterSideSize,
privacy_indicators_view()->GetPreferredSize().width());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/true, progress),
privacy_indicators_view()->GetPreferredSize().height());
shorter_side_shrink_animation()->Start();
AnimateToValue(shorter_side_shrink_animation(), progress);
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kBothSideShrink,
animation_state());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/false, progress),
privacy_indicators_view()->GetPreferredSize().width());
EXPECT_EQ(
GetExpectedSizeInShrinkAnimation(/*for_longer_side=*/true, progress),
privacy_indicators_view()->GetPreferredSize().height());
longer_side_shrink_animation()->End();
shorter_side_shrink_animation()->End();
// When finish, the view should have the size of a dot.
EXPECT_EQ(PrivacyIndicatorsTrayItemView::AnimationState::kIdle,
animation_state());
EXPECT_EQ(kPrivacyIndicatorsViewSize,
privacy_indicators_view()->GetPreferredSize().width());
EXPECT_EQ(kPrivacyIndicatorsViewSize,
privacy_indicators_view()->GetPreferredSize().height());
// All icon should not be visible.
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
EXPECT_FALSE(screen_share_icon()->GetVisible());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, StateChangeDuringAnimation) {
SetViewVisibleWithAnimation();
double progress = 0.5;
// Firstly, expand animation will be performed.
expand_animation()->Start();
AnimateToValue(expand_animation(), progress);
// Update state in mid animation, shouldn't crash anything.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
expand_animation()->End();
// After that shrink animations will be started.
longer_side_shrink_animation()->Start();
AnimateToValue(longer_side_shrink_animation(), progress);
// Update the state again, no crash expected.
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/true);
shorter_side_shrink_animation()->Start();
AnimateToValue(shorter_side_shrink_animation(), progress);
// The view should become invisible immediately after setting these states.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
privacy_indicators_view()->UpdateScreenShareStatus(
/*is_screen_sharing=*/false);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
// Clean up.
longer_side_shrink_animation()->End();
shorter_side_shrink_animation()->End();
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, MultipleAppsAccess) {
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
// When a new app accessing mic/cam, we will show the icons according to the
// access state of that particular app.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false, /*app_id=*/"app_id2");
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(camera_icon()->GetVisible());
EXPECT_FALSE(microphone_icon()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/true, /*app_id=*/"app_id3");
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_FALSE(camera_icon()->GetVisible());
EXPECT_TRUE(microphone_icon()->GetVisible());
// Indicator should still show when removing 1 and 2 app(s).
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false, /*app_id=*/"app_id2");
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false, /*app_id=*/"app_id3");
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
// Indicator should hide when removing all apps.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
EXPECT_FALSE(privacy_indicators_view()->GetVisible());
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, RecordShowTypeMetrics) {
auto check_histogram_record = [](bool is_camera_used, bool is_microphone_used,
bool is_screen_sharing,
PrivacyIndicatorsTrayItemView* view,
PrivacyIndicatorsTrayItemView::Type type) {
base::HistogramTester histograms;
UpdateCameraAndMicrophoneUsage(is_camera_used, is_microphone_used);
view->UpdateScreenShareStatus(is_screen_sharing);
histograms.ExpectBucketCount(kPrivacyIndicatorsShowTypeHistogramName, type,
1);
};
check_histogram_record(/*is_camera_used=*/true, /*is_microphone_used=*/false,
/*is_screen_sharing=*/false, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kCamera);
check_histogram_record(/*is_camera_used=*/false, /*is_microphone_used=*/true,
/*is_screen_sharing=*/false, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kMicrophone);
check_histogram_record(/*is_camera_used=*/false, /*is_microphone_used=*/false,
/*is_screen_sharing=*/true, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kScreenSharing);
check_histogram_record(
/*is_camera_used=*/true, /*is_microphone_used=*/true,
/*is_screen_sharing=*/false, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kCameraMicrophone);
check_histogram_record(
/*is_camera_used=*/true, /*is_microphone_used=*/false,
/*is_screen_sharing=*/true, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kCameraScreenSharing);
check_histogram_record(
/*is_camera_used=*/false, /*is_microphone_used=*/true,
/*is_screen_sharing=*/true, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kMicrophoneScreenSharing);
check_histogram_record(
/*is_camera_used=*/true, /*is_microphone_used=*/true,
/*is_screen_sharing=*/true, privacy_indicators_view(),
PrivacyIndicatorsTrayItemView::Type::kAllUsed);
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, RecordShowPerSessionMetrics) {
// Set up 2 displays. Note that only one instance should be recorded for the
// primary display when session changes.
UpdateDisplay("100x200,300x400");
int expected_count = 1;
// Show the indicator in the given `show_count` number of times.
auto trigger_show_indicator = [](int show_count) {
// Update the state of camera/microphone access so that the indicators on
// all displays show, then hide for `show_count` times.
for (auto i = 0; i < show_count; i++) {
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/true,
/*is_microphone_used=*/true);
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/false,
/*is_microphone_used=*/false);
}
};
base::HistogramTester histograms;
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::ACTIVE);
int expected_sample = 1;
trigger_show_indicator(expected_sample);
// After session changed, metrics should be recorded.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
histograms.ExpectBucketCount(kPrivacyIndicatorsShowPerSessionHistogramName,
expected_sample, expected_count);
expected_sample = 6;
trigger_show_indicator(expected_sample);
// After session changed, metrics should be recorded.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::ACTIVE);
histograms.ExpectBucketCount(kPrivacyIndicatorsShowPerSessionHistogramName,
expected_sample, expected_count);
expected_sample = 10;
trigger_show_indicator(expected_sample);
// After session changed, metrics should be recorded.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOGIN_PRIMARY);
histograms.ExpectBucketCount(kPrivacyIndicatorsShowPerSessionHistogramName,
expected_sample, expected_count);
}
// When multiple apps access camera and microphone, their histograms should
// update accordingly.
TEST_F(PrivacyIndicatorsTrayItemViewTest, RecordAppAccessSimultaneously) {
base::HistogramTester histograms;
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
histograms.ExpectBucketCount(kCountAppsAccessCameraHistogramName, 1, 1);
histograms.ExpectBucketCount(kCountAppsAccessMicrophoneHistogramName, 1, 0);
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/true, /*app_id=*/"app_id2");
histograms.ExpectBucketCount(kCountAppsAccessCameraHistogramName, 2, 1);
histograms.ExpectBucketCount(kCountAppsAccessMicrophoneHistogramName, 1, 1);
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/true,
/*is_microphone_used=*/true,
/*app_id=*/"app_id3");
histograms.ExpectBucketCount(kCountAppsAccessCameraHistogramName, 3, 1);
histograms.ExpectBucketCount(kCountAppsAccessMicrophoneHistogramName, 2, 1);
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, RecordRepeatedShows) {
// Set up 2 displays. Note that only one instance should be recorded for the
// primary display when session changes.
UpdateDisplay("100x200,300x400");
base::HistogramTester histograms;
auto flicker_indicator = [](int number_of_flicker,
base::test::TaskEnvironment* task_environment) {
// Makes the view flicker (show then hide) for `number_of_flicker` of times.
for (auto i = 0; i < number_of_flicker; i++) {
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/true,
/*is_microphone_used=*/true);
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/false,
/*is_microphone_used=*/false);
task_environment->FastForwardBy(base::Milliseconds(80));
}
task_environment->FastForwardBy(base::Milliseconds(100));
};
int expected_sample = 6;
flicker_indicator(expected_sample, task_environment());
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, expected_sample, 1);
// Makes one more flickering after 100ms. This flicker should not count
// towards the previous ones, but this will be counted in a bucket for 1 show.
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/true,
/*is_microphone_used=*/true);
UpdateCameraAndMicrophoneUsage(/*is_camera_used=*/false,
/*is_microphone_used=*/false);
task_environment()->FastForwardBy(base::Milliseconds(100));
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, expected_sample + 1,
0);
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, 1, 1);
// Make sure it works again.
flicker_indicator(8, task_environment());
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, 8, 1);
flicker_indicator(2, task_environment());
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, 2, 1);
flicker_indicator(1, task_environment());
histograms.ExpectBucketCount(kRepeatedShowsHistogramName, 1, 2);
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, RecordVisibilityDuration) {
// Set up 2 displays. Note that only one instance should be recorded for the
// primary display.
UpdateDisplay("100x200,300x400");
base::HistogramTester histograms;
auto start_time = base::Time::Now();
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
task_environment()->FastForwardBy(base::Milliseconds(100));
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
auto expected_sample1 = base::Time::Now() - start_time;
histograms.ExpectTimeBucketCount(kVisibilityDurationHistogramName,
expected_sample1, 1);
start_time = base::Time::Now();
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
task_environment()->FastForwardBy(base::Minutes(10));
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/false,
/*is_microphone_used=*/false);
histograms.ExpectTimeBucketCount(kVisibilityDurationHistogramName,
base::Time::Now() - start_time, 1);
// No new entries for previous bucket.
histograms.ExpectTimeBucketCount(kVisibilityDurationHistogramName,
expected_sample1, 1);
}
TEST_F(PrivacyIndicatorsTrayItemViewTest, IndicatorVisisbilityOnSecondDisplay) {
// Update usage when there's one display.
UpdateCameraAndMicrophoneUsage(
/*is_camera_used=*/true,
/*is_microphone_used=*/false);
ASSERT_TRUE(privacy_indicators_view()->GetVisible());
// Now set up 2 displays. The indicator should show on both displays.
UpdateDisplay("100x200,300x400");
EXPECT_TRUE(privacy_indicators_view()->GetVisible());
EXPECT_TRUE(GetSecondaryDisplayPrivacyIndicatorsView()->GetVisible());
}
} // namespace ash