chromium/ash/system/privacy_hub/camera_privacy_switch_controller_unittest.cc

// 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_hub/camera_privacy_switch_controller.h"

#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/privacy_hub/privacy_hub_controller.h"
#include "ash/system/privacy_hub/privacy_hub_metrics.h"
#include "ash/system/privacy_hub/privacy_hub_notification.h"
#include "ash/system/privacy_hub/privacy_hub_notification_controller.h"
#include "ash/system/privacy_hub/sensor_disabled_notification_delegate.h"
#include "ash/system/video_conference/fake_video_conference_tray_controller.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/ranges/algorithm.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"

using testing::_;

namespace ash {

namespace {

using Sensor = SensorDisabledNotificationDelegate::Sensor;

class MockSwitchAPI : public CameraPrivacySwitchAPI {
 public:
  MOCK_METHOD(void,
              SetCameraSWPrivacySwitch,
              (CameraSWPrivacySwitchSetting),
              (override));
};

class FakeSensorDisabledNotificationDelegate
    : public SensorDisabledNotificationDelegate {
 public:
  std::vector<std::u16string> GetAppsAccessingSensor(Sensor sensor) override {
    if (sensor == Sensor::kCamera) {
      return apps_accessing_camera_;
    }
    return {};
  }

  void LaunchAppAccessingCamera(const std::u16string& app_name) {
    apps_accessing_camera_.insert(apps_accessing_camera_.begin(), app_name);
  }

  void CloseAppAccessingCamera(const std::u16string& app_name) {
    auto it = base::ranges::find(apps_accessing_camera_, app_name);
    if (it != apps_accessing_camera_.end()) {
      apps_accessing_camera_.erase(it);
    }
  }

 private:
  std::vector<std::u16string> apps_accessing_camera_;
};

message_center::Notification* FindNotificationById(const std::string& id) {
  return message_center::MessageCenter::Get()->FindNotificationById(id);
}

// Mutes the camera on construction, and unmutes the camera when the object goes
// out of scope.
class ScopedCameraMuteToggler {
 public:
  explicit ScopedCameraMuteToggler(bool software_switch)
      : camera_privacy_switch_controller_(
            *CameraPrivacySwitchController::Get()),
        software_switch_(software_switch) {
    if (software_switch_) {
      Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
          prefs::kUserCameraAllowed, /*value=*/false);
    } else {
      camera_privacy_switch_controller_->OnCameraHWPrivacySwitchStateChanged(
          std::string(), cros::mojom::CameraPrivacySwitchState::ON);
    }
  }

  ~ScopedCameraMuteToggler() {
    if (software_switch_) {
      Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
          prefs::kUserCameraAllowed, /*value=*/true);
    } else {
      camera_privacy_switch_controller_->OnCameraHWPrivacySwitchStateChanged(
          std::string(), cros::mojom::CameraPrivacySwitchState::OFF);
    }
  }

 private:
  const raw_ref<CameraPrivacySwitchController>
      camera_privacy_switch_controller_;
  const bool software_switch_;
};

class MockFrontendAPI : public PrivacyHubDelegate {
 public:
  MOCK_METHOD(void, MicrophoneHardwareToggleChanged, (bool), (override));
  MOCK_METHOD(void, SetForceDisableCameraSwitch, (bool), (override));
};

}  // namespace

class PrivacyHubCameraTestBase : public AshTestBase,
                                 public testing::WithParamInterface<bool> {
 public:
  PrivacyHubCameraTestBase()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;
    if (IsVideoConferenceEnabled()) {
      fake_video_conference_tray_controller_ =
          std::make_unique<FakeVideoConferenceTrayController>();
      enabled_features.push_back(features::kFeatureManagementVideoConference);
    } else {
      disabled_features.push_back(features::kFeatureManagementVideoConference);
    }
    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }

  ~PrivacyHubCameraTestBase() override {
    fake_video_conference_tray_controller_.reset();
  }

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    auto mock_switch = std::make_unique<::testing::NiceMock<MockSwitchAPI>>();
    mock_switch_ = mock_switch.get();

    PrivacyHubController::Get()
        ->camera_controller()
        ->SetCameraPrivacySwitchAPIForTest(std::move(mock_switch));

    // Set up the fake `SensorDisabledNotificationDelegate`.
    // In production it is set only if Privacy Hub is enabled.
    scoped_delegate_ =
        std::make_unique<ScopedSensorDisabledNotificationDelegateForTest>(
            std::make_unique<FakeSensorDisabledNotificationDelegate>());
  }

  void TearDown() override {
    // We need to destroy the delegate while the Ash still exists.
    scoped_delegate_.reset();
    AshTestBase::TearDown();
  }

  bool IsVideoConferenceEnabled() { return GetParam(); }

  void SetUserPref(bool allowed) {
    Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
        prefs::kUserCameraAllowed, allowed);
  }

  bool GetUserPref() const {
    return Shell::Get()
        ->session_controller()
        ->GetActivePrefService()
        ->GetBoolean(prefs::kUserCameraAllowed);
  }

 protected:
  raw_ptr<::testing::NiceMock<MockSwitchAPI>, DanglingUntriaged> mock_switch_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<ScopedSensorDisabledNotificationDelegateForTest>
      scoped_delegate_;

  std::unique_ptr<FakeVideoConferenceTrayController>
      fake_video_conference_tray_controller_;
};

using PrivacyHubCameraSynchronizerTest = PrivacyHubCameraTestBase;

// Test reaction on user pref change (e.g. in UI).
TEST_P(PrivacyHubCameraSynchronizerTest, UserPrefChange) {
  const std::vector<bool> user_pref_sequence{false, true, true, false, true};
  const int number_of_changes = [&]() {
    int cnt = 0;
    bool current_val = true;  // default value for camera-enabled is true
    for (const bool pref_val : user_pref_sequence) {
      if (pref_val != current_val) {
        ++cnt;
        current_val = pref_val;
      }
    }
    return cnt;
  }();

  CameraSWPrivacySwitchSetting captured_val;
  EXPECT_CALL(*mock_switch_, SetCameraSWPrivacySwitch(_))
      .Times(number_of_changes)
      .WillRepeatedly(testing::SaveArg<0>(&captured_val));

  for (const bool pref_val : user_pref_sequence) {
    SetUserPref(pref_val);
    // User toggle ON means the camera is DISABLED.
    const CameraSWPrivacySwitchSetting expected_val =
        pref_val ? CameraSWPrivacySwitchSetting::kEnabled
                 : CameraSWPrivacySwitchSetting::kDisabled;
    EXPECT_EQ(captured_val, expected_val);
  }
}

TEST_P(PrivacyHubCameraSynchronizerTest, OnCameraSoftwarePrivacySwitchChanged) {
  // When `prefs::kUserCameraAllowed` is true and CrOS Camera Service
  // communicates the SW privacy switch state as UNKNOWN or ON, the states
  // mismatch and `SetCameraSWPrivacySwitch(kEnabled)` should be called to
  // correct the mismatch.
  EXPECT_CALL(*mock_switch_,
              SetCameraSWPrivacySwitch(CameraSWPrivacySwitchSetting::kEnabled))
      .Times(::testing::Exactly(3));
  SetUserPref(true);
  PrivacyHubController::Get()
      ->camera_controller()
      ->OnCameraSWPrivacySwitchStateChanged(
          cros::mojom::CameraPrivacySwitchState::UNKNOWN);
  PrivacyHubController::Get()
      ->camera_controller()
      ->OnCameraSWPrivacySwitchStateChanged(
          cros::mojom::CameraPrivacySwitchState::ON);

  if (IsVideoConferenceEnabled()) {
    // When `prefs::kUserCameraAllowed` is false and CrOS Camera Service
    // communicates the SW privacy switch state as UNKNOWN or OFF, the states
    // mismatch and `SetCameraSWPrivacySwitch(kDisabled)` should be called to
    // correct the mismatch.
    EXPECT_CALL(*mock_switch_, SetCameraSWPrivacySwitch(
                                   CameraSWPrivacySwitchSetting::kDisabled))
        .Times(::testing::Exactly(3));
    SetUserPref(false);
    PrivacyHubController::Get()
        ->camera_controller()
        ->OnCameraSWPrivacySwitchStateChanged(
            cros::mojom::CameraPrivacySwitchState::UNKNOWN);
    PrivacyHubController::Get()
        ->camera_controller()
        ->OnCameraSWPrivacySwitchStateChanged(
            cros::mojom::CameraPrivacySwitchState::OFF);

    // When the SW privacy switch states match in Privacy Hub and CrOS Camera
    // Service, `SetCameraSWPrivacySwitch()` should not be called.
    EXPECT_CALL(*mock_switch_, SetCameraSWPrivacySwitch(_))
        .Times(::testing::Exactly(2));

    // When `prefs::kUserCameraAllowed` is true and CrOS Camera Service
    // communicates the SW privacy switch state as OFF, the states match and
    // `SetCameraSWPrivacySwitch()` should not be called.
    SetUserPref(true);
    PrivacyHubController::Get()
        ->camera_controller()
        ->OnCameraSWPrivacySwitchStateChanged(
            cros::mojom::CameraPrivacySwitchState::OFF);

    // When `prefs::kUserCameraAllowed` is false and CrOS Camera Service
    // communicates the SW privacy switch state as ON, the states match and
    // `SetCameraSWPrivacySwitch()` should not be called.
    SetUserPref(false);
    PrivacyHubController::Get()
        ->camera_controller()
        ->OnCameraSWPrivacySwitchStateChanged(
            cros::mojom::CameraPrivacySwitchState::ON);
  }
}

class NotificationTestBase : public PrivacyHubCameraTestBase {
 public:
  void SetUp() override {
    PrivacyHubCameraTestBase::SetUp();
    controller_ = CameraPrivacySwitchController::Get();
  }

  void LaunchAppAccessingCamera(const std::u16string& app_name) {
    delegate()->LaunchAppAccessingCamera(app_name);
    controller_->ActiveApplicationsChanged(/*application_added=*/true);
  }

  void CloseAppAccessingCamera(const std::u16string& app_name) {
    delegate()->CloseAppAccessingCamera(app_name);
    controller_->ActiveApplicationsChanged(/*application_added=*/false);
  }

  message_center::Notification* GetSWSwitchNotification() {
    return FindNotificationById(
        PrivacyHubNotificationController::kCombinedNotificationId);
  }

  FakeSensorDisabledNotificationDelegate* delegate() {
    return static_cast<FakeSensorDisabledNotificationDelegate*>(
        PrivacyHubNotificationController::Get()
            ->sensor_disabled_notification_delegate());
  }

  raw_ptr<CameraPrivacySwitchController, DanglingUntriaged> controller_;
  const base::HistogramTester histogram_tester_;
};

class PrivacyHubCameraControllerTest : public NotificationTestBase {};

TEST_P(PrivacyHubCameraControllerTest,
       OnCameraHardwarePrivacySwitchChangedMultipleCameras) {
  CameraPrivacySwitchController& controller =
      *CameraPrivacySwitchController::Get();

  // We have 2 cameras in the system.
  controller.OnCameraCountChanged(2);
  // Camera is enabled in Privacy Hub.
  SetUserPref(true);

  // Somebody switched the camera off by the hardware switch.
  controller.OnCameraHWPrivacySwitchStateChanged(
      std::string(), cros::mojom::CameraPrivacySwitchState::OFF);
  // Is shall be no SW notification.
  EXPECT_FALSE(GetSWSwitchNotification());

  // Somebody switched the camera on by the hardware switch.
  controller.OnCameraHWPrivacySwitchStateChanged(
      std::string(), cros::mojom::CameraPrivacySwitchState::ON);
  // Is shall be no SW notification.
  EXPECT_FALSE(GetSWSwitchNotification());

  // User pref didn't change.
  EXPECT_TRUE(GetUserPref());
  // We didn't log any notification clicks so far.
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                false),
            0);
}

TEST_P(PrivacyHubCameraControllerTest,
       OnCameraHardwarePrivacySwitchChangedOneCamera) {
  CameraPrivacySwitchController& controller =
      *CameraPrivacySwitchController::Get();
  // We have 1 camera in the system.
  controller.OnCameraCountChanged(1);
  // Camera is enabled in Privacy Hub.
  SetUserPref(true);

  // Somebody switched the camera off by the hardware switch.
  controller.OnCameraHWPrivacySwitchStateChanged(
      std::string(), cros::mojom::CameraPrivacySwitchState::OFF);
  // Is shall be no SW notification.
  EXPECT_FALSE(GetSWSwitchNotification());

  // Switching the hardware switch back again.
  controller.OnCameraHWPrivacySwitchStateChanged(
      std::string(), cros::mojom::CameraPrivacySwitchState::ON);
  // Is shall be no SW notification.
  EXPECT_FALSE(GetSWSwitchNotification());

  // This didn't cause any change in the setting toggle.
  EXPECT_TRUE(GetUserPref());
  // There were no changes to the histograms.
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                false),
            0);
}

// This test is a regression test for b/253407315
TEST_P(PrivacyHubCameraControllerTest,
       OnCameraHardwarePrivacySwitchChangedNotificationClearing) {
  CameraPrivacySwitchController& controller =
      *CameraPrivacySwitchController::Get();
  SetUserPref(true);
  controller.OnCameraCountChanged(2);

  controller.OnCameraHWPrivacySwitchStateChanged(
      "0", cros::mojom::CameraPrivacySwitchState::ON);

  LaunchAppAccessingCamera(u"app_name");

  // Notification should be cleared when hardware mute is disabled
  controller.OnCameraHWPrivacySwitchStateChanged(
      "0", cros::mojom::CameraPrivacySwitchState::OFF);

  // Parameters shall not be changed
  EXPECT_TRUE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest,
       CameraOffNotificationRemoveViaClickOnButton) {
  SetUserPref(false);
  message_center::MessageCenter* const message_center =
      message_center::MessageCenter::Get();
  ASSERT_TRUE(message_center);
  ASSERT_FALSE(GetSWSwitchNotification());

  // An application starts accessing the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  if (IsVideoConferenceEnabled()) {
    // No Notification should be sent. Return early because the rest of the test
    // tests notification behavior.
    EXPECT_FALSE(GetSWSwitchNotification());
    return;
  }
  // A notification should be fired.
  EXPECT_TRUE(GetSWSwitchNotification());
  EXPECT_FALSE(GetUserPref());

  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  // Enabling camera via clicking on the button should clear the notification
  message_center->ClickOnNotificationButton(
      PrivacyHubNotificationController::kCombinedNotificationId, 0);
  EXPECT_TRUE(GetUserPref());
  EXPECT_FALSE(GetSWSwitchNotification());
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            1);
}

TEST_P(PrivacyHubCameraControllerTest,
       CameraOffNotificationRemoveViaClickOnBody) {
  SetUserPref(false);
  controller_->OnCameraCountChanged(2);
  message_center::MessageCenter* const message_center =
      message_center::MessageCenter::Get();
  ASSERT_TRUE(message_center);
  ASSERT_FALSE(GetSWSwitchNotification());

  // An application starts accessing the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  if (IsVideoConferenceEnabled()) {
    EXPECT_FALSE(GetSWSwitchNotification());
  } else {
    // A notification should be fired.
    EXPECT_TRUE(GetSWSwitchNotification());
  }
  EXPECT_FALSE(GetUserPref());

  if (!IsVideoConferenceEnabled()) {
    // Enabling camera via clicking on the body should open the privacy hub
    // settings page.
    message_center->ClickOnNotification(
        PrivacyHubNotificationController::kCombinedNotificationId);

    // The user pref should not be changed.
    EXPECT_FALSE(GetUserPref());
    EXPECT_FALSE(GetSWSwitchNotification());
  }

  SetUserPref(true);

  // Flip the hardware switch.
  controller_->OnCameraHWPrivacySwitchStateChanged(
      "0", cros::mojom::CameraPrivacySwitchState::ON);

  // No notification is fired for switch changes during the capture session.
  // But one will be fired if a new session starts.
  EXPECT_FALSE(GetSWSwitchNotification());
  EXPECT_TRUE(GetUserPref());

  // Adds a second application.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  // Is shall be no SW notification.
  EXPECT_FALSE(GetSWSwitchNotification());

  EXPECT_TRUE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest, CameraOffNotificationRemoveViaUserPref) {
  SetUserPref(false);
  ASSERT_FALSE(GetSWSwitchNotification());

  // An application starts accessing the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);
  if (IsVideoConferenceEnabled()) {
    EXPECT_FALSE(GetSWSwitchNotification());
  } else {
    // A notification should be fired.
    EXPECT_TRUE(GetSWSwitchNotification());
  }
  EXPECT_FALSE(GetUserPref());

  // Enabling camera via the user pref should clear the notification.
  SetUserPref(true);
  EXPECT_TRUE(GetUserPref());
  EXPECT_FALSE(GetSWSwitchNotification());
}

TEST_P(PrivacyHubCameraControllerTest, InSessionSwitchNotification) {
  SetUserPref(true);
  message_center::MessageCenter* const message_center =
      message_center::MessageCenter::Get();
  ASSERT_TRUE(message_center);
  message_center->RemoveNotification(
      PrivacyHubNotificationController::kCombinedNotificationId, false);

  // An application starts accessing the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);
  // Disable the camera after the application count has changed.
  SetUserPref(false);
  // A notification should not be fired (when pref set to false while an app was
  // running).
  EXPECT_FALSE(GetSWSwitchNotification());
  EXPECT_FALSE(GetUserPref());

  // Trigger the notification by simulating a second application accessing the
  // camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  // If VC enabled - no notification to be shown.
  if (IsVideoConferenceEnabled()) {
    EXPECT_FALSE(GetSWSwitchNotification());
    // No notifications shown - nothing left to test.
    return;
  }

  // Otherwise - a notification to be shown if a new app started.
  EXPECT_TRUE(GetSWSwitchNotification());
  EXPECT_FALSE(GetUserPref());

  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  // Enabling camera via clicking on the button should clear the notification.
  message_center->ClickOnNotificationButton(
      PrivacyHubNotificationController::kCombinedNotificationId, 0);
  EXPECT_TRUE(GetUserPref());
  EXPECT_FALSE(GetSWSwitchNotification());
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            1);
}

// Tests if camera software switch notification is removed when the number of
// apps accessing the camera becomes 0.
TEST_P(PrivacyHubCameraControllerTest,
       NotificationRemovedWhenNoActiveApplication) {
  SetUserPref(true);

  // The notification should not be in the message center initially.
  EXPECT_FALSE(GetSWSwitchNotification());

  // This is the effect of an application starting to access the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  // Disabling camera using the software switch.
  SetUserPref(false);

  // No notification pops up if just the switch is modified.
  EXPECT_FALSE(GetSWSwitchNotification());

  controller_->ActiveApplicationsChanged(/*application_added=*/true);

  if (IsVideoConferenceEnabled()) {
    EXPECT_FALSE(GetSWSwitchNotification());
  } else {
    EXPECT_TRUE(GetSWSwitchNotification());
  }

  // The only active application stops accessing the camera the camera.
  controller_->ActiveApplicationsChanged(/*application_added=*/false);

  // The notification should still be shown, because two apps were accessing
  // the camera.
  if (IsVideoConferenceEnabled()) {
    EXPECT_FALSE(GetSWSwitchNotification());
  } else {
    EXPECT_TRUE(GetSWSwitchNotification());
  }

  controller_->ActiveApplicationsChanged(/*application_added=*/false);

  // Existing notification should be removed as the number of active
  // applications is 0 now.
  EXPECT_FALSE(GetSWSwitchNotification());
}

// Tests if the camera software switch notification contains proper text.
TEST_P(PrivacyHubCameraControllerTest, NotificationText) {
  if (IsVideoConferenceEnabled()) {
    return;
  }
  // Disabling camera using the software switch.
  SetUserPref(false);
  EXPECT_FALSE(GetSWSwitchNotification());

  // Launch app1 that's accessing camera, a notification should be displayed
  // with the application name in the notification body.
  const std::u16string app1 = u"app1";
  LaunchAppAccessingCamera(app1);

  message_center::Notification* notification = GetSWSwitchNotification();
  ASSERT_TRUE(notification);
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_TITLE),
      notification->title());
  EXPECT_EQ(
      l10n_util::GetStringFUTF16(
          IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
          app1),
      notification->message());

  // Launch app2 that's also accessing camera, a notification should be
  // displayed again with both of the application names in the notification
  // body.
  const std::u16string app2 = u"app2";
  LaunchAppAccessingCamera(app2);

  notification = GetSWSwitchNotification();
  ASSERT_TRUE(notification);
  EXPECT_EQ(
      l10n_util::GetStringFUTF16(
          IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
          app1, app2),
      notification->message());

  // Launch app3 that's also accessing camera, a notification should be
  // displayed again with generic text.
  const std::u16string app3 = u"app3";
  LaunchAppAccessingCamera(app3);

  notification = GetSWSwitchNotification();
  ASSERT_TRUE(notification);
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE),
            notification->message());

  // Close one of the applications. The notification should be updated to
  // contain the name of the two remaining applications.
  CloseAppAccessingCamera(app2);

  notification = GetSWSwitchNotification();
  ASSERT_TRUE(notification);
  EXPECT_EQ(
      l10n_util::GetStringFUTF16(
          IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
          app1, app3),
      notification->message());
}

TEST_P(PrivacyHubCameraControllerTest, MetricCollection) {
  if (IsVideoConferenceEnabled()) {
    return;
  }
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                false),
            0);

  PrivacyHubNotificationController::SetAndLogSensorPreferenceFromNotification(
      Sensor::kCamera, false);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            0);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                false),
            1);

  PrivacyHubNotificationController::SetAndLogSensorPreferenceFromNotification(
      Sensor::kCamera, true);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                true),
            1);
  EXPECT_EQ(histogram_tester_.GetBucketCount(
                privacy_hub_metrics::
                    kPrivacyHubCameraEnabledFromNotificationHistogram,
                false),
            1);
}

TEST_P(PrivacyHubCameraControllerTest,
       ForceDisableAccessShouldDisableUserPref) {
  SetUserPref(true);

  auto& controller = *CameraPrivacySwitchController::Get();

  controller.SetForceDisableCameraAccess(true);

  EXPECT_FALSE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest,
       ForceDisableAccessShouldAlsoWorkBeforeUserPrefIsRegistered) {
  SetUserPref(true);

  // Create a local version of the controller, so we can delay registration of
  // the user pref service.
  CameraPrivacySwitchController controller;

  controller.SetForceDisableCameraAccess(true);

  controller.OnActiveUserPrefServiceChanged(
      Shell::Get()->session_controller()->GetActivePrefService());

  EXPECT_FALSE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest,
       ForceDisableAccessShouldPreventPrefChanges) {
  auto& controller = *CameraPrivacySwitchController::Get();
  controller.SetForceDisableCameraAccess(true);

  SetUserPref(true);
  EXPECT_FALSE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest, ShouldBeAbleToStopForceDisableAccess) {
  auto& controller = *CameraPrivacySwitchController::Get();

  controller.SetForceDisableCameraAccess(true);
  ASSERT_TRUE(controller.IsCameraAccessForceDisabled());

  controller.SetForceDisableCameraAccess(false);
  ASSERT_FALSE(controller.IsCameraAccessForceDisabled());
}

TEST_P(PrivacyHubCameraControllerTest,
       ShouldBeAbleToChangePrefAfterStoppingForceDisableAccess) {
  auto& controller = *CameraPrivacySwitchController::Get();
  controller.SetForceDisableCameraAccess(true);
  controller.SetForceDisableCameraAccess(false);

  SetUserPref(true);
  EXPECT_TRUE(GetUserPref());
}

TEST_P(PrivacyHubCameraControllerTest,
       StoppingForceDisableAccessShouldNotCrashEvenBeforeUserPrefIsRegistered) {
  // Create a local version of the controller, so we can delay registration of
  // the user pref service.
  CameraPrivacySwitchController controller;

  // Neither of these should crash (by trying to access the user pref).
  controller.SetForceDisableCameraAccess(true);
  controller.SetForceDisableCameraAccess(false);
}

TEST_P(PrivacyHubCameraControllerTest,
       ShouldRestorePreviousPrefAfterForceDisableAccess) {
  auto& controller = *CameraPrivacySwitchController::Get();

  for (const bool previous_value : {true, false}) {
    SetUserPref(previous_value);
    controller.SetForceDisableCameraAccess(true);
    controller.SetForceDisableCameraAccess(false);
    EXPECT_EQ(GetUserPref(), previous_value);
  }
}

TEST_P(PrivacyHubCameraControllerTest,
       ShouldRestorePreviousPrefEvenAfterRecreatingTheController) {
  // This test ensures that we restore the previous pref value, even if
  // it was force disabled and then Chrome crashed.

  for (const bool previous_value : {true, false}) {
    SetUserPref(previous_value);

    std::optional<CameraPrivacySwitchController> controller;

    // Create the controller.
    controller.emplace();
    controller->OnActiveUserPrefServiceChanged(
        Shell::Get()->session_controller()->GetActivePrefService());

    // Force disable camera access.
    controller->SetForceDisableCameraAccess(true);

    // Simulate crash and restart by destroying and recreating the controller.
    controller.emplace();
    controller->OnActiveUserPrefServiceChanged(
        Shell::Get()->session_controller()->GetActivePrefService());

    // The previous pref value should be restored.
    EXPECT_EQ(GetUserPref(), previous_value);
  }
}

TEST_P(PrivacyHubCameraControllerTest,
       ForceDisableAccessShouldDisableUiSwitch) {
  ::testing::StrictMock<MockFrontendAPI> mock_frontend;
  Shell::Get()->privacy_hub_controller()->SetFrontend(&mock_frontend);
  auto& controller = *CameraPrivacySwitchController::Get();

  EXPECT_CALL(mock_frontend, SetForceDisableCameraSwitch(true));

  controller.SetForceDisableCameraAccess(true);

  Shell::Get()->privacy_hub_controller()->SetFrontend(nullptr);
}

TEST_P(PrivacyHubCameraControllerTest,
       StoppingForceDisableAccessShouldReenableUiSwitch) {
  ::testing::StrictMock<MockFrontendAPI> mock_frontend;
  Shell::Get()->privacy_hub_controller()->SetFrontend(&mock_frontend);
  auto& controller = *CameraPrivacySwitchController::Get();

  EXPECT_CALL(mock_frontend, SetForceDisableCameraSwitch(false));

  controller.SetForceDisableCameraAccess(false);

  Shell::Get()->privacy_hub_controller()->SetFrontend(nullptr);
}

TEST_P(PrivacyHubCameraControllerTest,
       ShouldRestorePreviousPrefEvenAfterForceDisableAccessTwice) {
  // This test ensures that force disabling camera access a second time will
  // not overwrite the stored previous value (which should keep reflecting the
  // state from before we force disabled camera access the first time).

  for (const bool previous_value : {true, false}) {
    SetUserPref(previous_value);

    std::optional<CameraPrivacySwitchController> controller;

    // Create the controller.
    controller.emplace();
    controller->OnActiveUserPrefServiceChanged(
        Shell::Get()->session_controller()->GetActivePrefService());

    // Force disable camera access.
    controller->SetForceDisableCameraAccess(true);
    // Twice.
    controller->SetForceDisableCameraAccess(true);

    // Simulate crash and restart by destroying and recreating the controller.
    controller.emplace();
    controller->OnActiveUserPrefServiceChanged(
        Shell::Get()->session_controller()->GetActivePrefService());

    // The previous pref value should be restored.
    EXPECT_EQ(GetUserPref(), previous_value);
  }
}

class VideoConferenceCameraControllerTest : public NotificationTestBase {};

// With VcControls enabled, tests that no notification shows up if the switches
// are toggled when the number of capturing apps does not change.
TEST_P(VideoConferenceCameraControllerTest,
       NoNotificationDuringCaptureSession) {
  if (!IsVideoConferenceEnabled()) {
    return;
  }

  const std::u16string app_name = u"app_name";
  for (bool app_running : {false, true}) {
    if (app_running) {
      LaunchAppAccessingCamera(app_name);
      EXPECT_FALSE(GetSWSwitchNotification());
    }
    // Disable camera using the software switch. No notification should show.
    SetUserPref(/*allowed=*/false);

    EXPECT_FALSE(GetSWSwitchNotification());

    // Re-enable the camera, there still should be no notification.
    SetUserPref(/*allowed=*/true);

    EXPECT_FALSE(GetSWSwitchNotification());

    // Repeat the test with the hardware switch.
    auto* controller = CameraPrivacySwitchController::Get();
    controller->OnCameraHWPrivacySwitchStateChanged(
        std::string(), cros::mojom::CameraPrivacySwitchState::ON);

    // It shall not cause SW notification
    EXPECT_FALSE(GetSWSwitchNotification());

    controller->OnCameraHWPrivacySwitchStateChanged(
        std::string(), cros::mojom::CameraPrivacySwitchState::OFF);

    // It shall not cause SW notification
    EXPECT_FALSE(GetSWSwitchNotification());

    if (app_running) {
      CloseAppAccessingCamera(app_name);
    }
  }
}

// With VcControls or Privacy Indicators enabled, tests that a notification
// shows up when the number of apps capturing the camera increases if the switch
// is muted.
TEST_P(VideoConferenceCameraControllerTest,
       NotificationShowsIfNewAppStartsCapturing) {
  for (bool software_switch : {true, false}) {
    SCOPED_TRACE(::testing::Message()
                 << "software_switch: " << software_switch);
    auto scoped_software_switch_toggler =
        ScopedCameraMuteToggler(/*software_switch=*/software_switch);

    // independently from the software_switch - shall be no notification.
    EXPECT_FALSE(GetSWSwitchNotification());

    // Simulate an app accessing the camera, a notification should show.
    LaunchAppAccessingCamera(u"app_name");

    if (IsVideoConferenceEnabled()) {
      // No notification when VC is enabled.
      EXPECT_FALSE(GetSWSwitchNotification());
    } else {
      // No VC - notification to be shown only for SW switch.
      if (software_switch) {
        EXPECT_TRUE(GetSWSwitchNotification());
      } else {
        EXPECT_FALSE(GetSWSwitchNotification());
      }
    }
  }
}

// With VcControls or Privacy Indicators enabled, tests that turning camera
// access back on hides the notification.
TEST_P(VideoConferenceCameraControllerTest,
       EnablingCameraAccessHidesNotification) {
  for (bool software_switch : {true, false}) {
    auto scoped_software_switch_toggler =
        std::make_unique<ScopedCameraMuteToggler>(
            /*software_switch=*/software_switch);

    // Simulate an app accessing the camera.
    LaunchAppAccessingCamera(u"app_name");

    if (IsVideoConferenceEnabled()) {
      // No notification when VC is enabled.
      EXPECT_FALSE(GetSWSwitchNotification());
    } else {
      // No VC - notification to be shown only for SW switch.
      if (software_switch) {
        EXPECT_TRUE(GetSWSwitchNotification());
      } else {
        EXPECT_FALSE(GetSWSwitchNotification());
      }
    }
    // Reverses the switch, the notification should go away.
    scoped_software_switch_toggler.reset();

    EXPECT_FALSE(GetSWSwitchNotification());
  }
}

// With VcControls or Privacy Indicators enabled, tests that a notification
// shows up if muted and then a capture starts.
TEST_P(VideoConferenceCameraControllerTest,
       NotificationWhenMutedOnCaptureStart) {
  for (bool software_switch : {true, false}) {
    auto scoped_software_switch_toggler =
        std::make_unique<ScopedCameraMuteToggler>(
            /*software_switch=*/software_switch);
    const std::u16string app_name = u"app_name";
    LaunchAppAccessingCamera(app_name);

    if (IsVideoConferenceEnabled()) {
      // No notification when VC is enabled.
      EXPECT_FALSE(GetSWSwitchNotification());
    } else {
      // No VC - notification to be shown only for SW switch.
      if (software_switch) {
        EXPECT_TRUE(GetSWSwitchNotification());
      } else {
        EXPECT_FALSE(GetSWSwitchNotification());
      }
    }

    CloseAppAccessingCamera(app_name);

    EXPECT_FALSE(GetSWSwitchNotification());
  }
}

// With VcControls or Privacy Indicators enabled, tests that a notification goes
// away when capturing apps drop to 0.
TEST_P(VideoConferenceCameraControllerTest,
       NotificationGoesAwayWhenAppsGoToZero) {
  for (bool software_switch : {true, false}) {
    auto scoped_software_switch_toggler =
        std::make_unique<ScopedCameraMuteToggler>(
            /*software_switch=*/software_switch);
    const std::u16string app_name = u"app_name";
    LaunchAppAccessingCamera(app_name);

    if (IsVideoConferenceEnabled()) {
      // No notification when VC is enabled.
      EXPECT_FALSE(GetSWSwitchNotification());
    } else {
      // No VC - notification to be shown only for SW switch.
      if (software_switch) {
        EXPECT_TRUE(GetSWSwitchNotification());
      } else {
        EXPECT_FALSE(GetSWSwitchNotification());
      }
    }

    // Remove the capturing app, the notification should disappear.
    CloseAppAccessingCamera(app_name);

    EXPECT_FALSE(GetSWSwitchNotification());
  }
}

INSTANTIATE_TEST_SUITE_P(All,
                         PrivacyHubCameraSynchronizerTest,
                         /*IsVideoConferenceEnabled=*/testing::Bool());

INSTANTIATE_TEST_SUITE_P(All,
                         PrivacyHubCameraControllerTest,
                         /*IsVideoConferenceEnabled=*/testing::Bool());

INSTANTIATE_TEST_SUITE_P(All,
                         VideoConferenceCameraControllerTest,
                         /*IsVideoConferenceEnabled=*/testing::Bool());

}  // namespace ash