chromium/ash/system/privacy_hub/camera_privacy_switch_controller.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 <cstddef>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/session/session_observer.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/system_notification_controller.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/sequence_checker.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "components/prefs/pref_service.h"
#include "media/capture/video/chromeos/camera_hal_dispatcher_impl.h"
#include "media/capture/video/chromeos/mojom/cros_camera_service.mojom-shared.h"

namespace ash {

namespace {

// Wraps and adapts the VCD API.
// It is used for dependency injection, so that we can write
// mock tests for CameraController easily.
class VCDPrivacyAdapter : public CameraPrivacySwitchAPI {
 public:
  // CameraPrivacySwitchAPI:
  void SetCameraSWPrivacySwitch(CameraSWPrivacySwitchSetting) override;
};

void VCDPrivacyAdapter::SetCameraSWPrivacySwitch(
    CameraSWPrivacySwitchSetting camera_switch_setting) {
  switch (camera_switch_setting) {
    case CameraSWPrivacySwitchSetting::kEnabled: {
      media::CameraHalDispatcherImpl::GetInstance()
          ->SetCameraSWPrivacySwitchState(
              cros::mojom::CameraPrivacySwitchState::OFF);
      break;
    }
    case CameraSWPrivacySwitchSetting::kDisabled: {
      media::CameraHalDispatcherImpl::GetInstance()
          ->SetCameraSWPrivacySwitchState(
              cros::mojom::CameraPrivacySwitchState::ON);
      break;
    }
  }
}

const base::TimeDelta kCameraLedFallbackNotificationExtensionPeriod =
    base::Seconds(30);

}  // namespace

CameraPrivacySwitchController::CameraPrivacySwitchController()
    : switch_api_(std::make_unique<VCDPrivacyAdapter>()) {
  Shell::Get()->session_controller()->AddObserver(this);
}

CameraPrivacySwitchController::~CameraPrivacySwitchController() {
  Shell::Get()->session_controller()->RemoveObserver(this);
  media::CameraHalDispatcherImpl::GetInstance()
      ->RemoveCameraPrivacySwitchObserver(this);
}

void CameraPrivacySwitchController::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  // Subscribing again to pref changes.
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(pref_service);
  pref_change_registrar_->Add(
      prefs::kUserCameraAllowed,
      base::BindRepeating(&CameraPrivacySwitchController::OnPreferenceChanged,
                          base::Unretained(this)));

  if (!is_camera_observer_added_) {
    // Subscribe to the camera HW/SW privacy switch events.
    auto device_id_to_privacy_switch_state =
        media::CameraHalDispatcherImpl::GetInstance()
            ->AddCameraPrivacySwitchObserver(this);
    is_camera_observer_added_ = true;
  }

  if (force_disable_camera_access_) {
    StorePreviousPrefValue();
    prefs().SetBoolean(prefs::kUserCameraAllowed, false);
  } else {
    // It's possible we crashed while force disable camera access was enabled,
    // in which case we need to restore the previous pref value.
    RestorePreviousPrefValueMaybe();
  }

  // To ensure consistent values between the user pref and camera backend.
  OnPreferenceChanged(prefs::kUserCameraAllowed);
}

void CameraPrivacySwitchController::OnCameraSWPrivacySwitchStateChanged(
    // This makes sure that the backend state is in sync with the pref.
    // The backend service sometimes may have a wrong camera switch state after
    // restart. This is necessary to correct it.
    cros::mojom::CameraPrivacySwitchState state) {
  const CameraSWPrivacySwitchSetting pref_val = GetUserSwitchPreference();
  // Note that camera ON means privacy switch OFF.
  cros::mojom::CameraPrivacySwitchState pref_state =
      pref_val == CameraSWPrivacySwitchSetting::kEnabled
          ? cros::mojom::CameraPrivacySwitchState::OFF
          : cros::mojom::CameraPrivacySwitchState::ON;
  if (state != pref_state) {
    SetCameraSWPrivacySwitch(pref_val);
  }
}

void CameraPrivacySwitchController::OnPreferenceChanged(
    const std::string& pref_name) {
  DCHECK_EQ(pref_name, prefs::kUserCameraAllowed);

  // Always remove the sensor disabled notification if the sensor was unmuted.
  if (GetUserSwitchPreference() == CameraSWPrivacySwitchSetting::kEnabled) {
    PrivacyHubNotificationController::Get()->RemoveSoftwareSwitchNotification(
        SensorDisabledNotificationDelegate::Sensor::kCamera);
  }

  if (force_disable_camera_access_ &&
      GetUserSwitchPreference() != CameraSWPrivacySwitchSetting::kDisabled) {
    prefs().SetBoolean(prefs::kUserCameraAllowed, false);
  }

  // This needs to be called after RemoveSoftwareSwitchNotification() as that
  // call can change the pref value.
  const CameraSWPrivacySwitchSetting pref_val = GetUserSwitchPreference();
  switch_api_->SetCameraSWPrivacySwitch(pref_val);
}

CameraSWPrivacySwitchSetting
CameraPrivacySwitchController::GetUserSwitchPreference() const {
  const bool allowed = prefs().GetBoolean(prefs::kUserCameraAllowed);

  return allowed ? CameraSWPrivacySwitchSetting::kEnabled
                 : CameraSWPrivacySwitchSetting::kDisabled;
}

void CameraPrivacySwitchController::SetCameraPrivacySwitchAPIForTest(
    std::unique_ptr<CameraPrivacySwitchAPI> switch_api) {
  DCHECK(switch_api);
  switch_api_ = std::move(switch_api);
}

void CameraPrivacySwitchController::SetCameraSWPrivacySwitch(
    CameraSWPrivacySwitchSetting value) {
  switch_api_->SetCameraSWPrivacySwitch(value);
}

void CameraPrivacySwitchController::SetUserSwitchPreference(
    CameraSWPrivacySwitchSetting value) {
  prefs().SetBoolean(prefs::kUserCameraAllowed,
                     value == CameraSWPrivacySwitchSetting::kEnabled);
}

void CameraPrivacySwitchController::SetFrontend(PrivacyHubDelegate* frontend) {
  frontend_ = frontend;
}

void CameraPrivacySwitchController::SetForceDisableCameraAccess(
    bool new_value) {
  force_disable_camera_access_ = new_value;
  if (pref_change_registrar_) {
    if (new_value) {
      StorePreviousPrefValue();
      prefs().SetBoolean(prefs::kUserCameraAllowed, false);
    } else {
      RestorePreviousPrefValueMaybe();
    }
  }

  if (frontend_) {
    frontend_->SetForceDisableCameraSwitch(new_value);
  }
}

bool CameraPrivacySwitchController::IsCameraAccessForceDisabled() const {
  return force_disable_camera_access_;
}

void CameraPrivacySwitchController::StorePreviousPrefValue() {
  if (prefs().HasPrefPath(prefs::kUserCameraAllowedPreviousValue)) {
    // Do not overwrite previous stored value, otherwise force disabling
    // camera access twice in a row will not properly restore the previous
    // value.
    return;
  }

  prefs().SetBoolean(prefs::kUserCameraAllowedPreviousValue,
                     prefs().GetBoolean(prefs::kUserCameraAllowed));
}

void CameraPrivacySwitchController::RestorePreviousPrefValueMaybe() {
  // If a previous value was stored, restore it and then clear the stored
  // previous value so we do not keep restoring it.
  if (prefs().HasPrefPath(prefs::kUserCameraAllowedPreviousValue)) {
    prefs().SetBoolean(
        prefs::kUserCameraAllowed,
        prefs().GetBoolean(prefs::kUserCameraAllowedPreviousValue));

    prefs().ClearPref(prefs::kUserCameraAllowedPreviousValue);
  }
}

PrefService& CameraPrivacySwitchController::prefs() {
  CHECK(pref_change_registrar_);
  return CHECK_DEREF(pref_change_registrar_->prefs());
}

const PrefService& CameraPrivacySwitchController::prefs() const {
  CHECK(pref_change_registrar_);
  return CHECK_DEREF(pref_change_registrar_->prefs());
}

// static
CameraPrivacySwitchController* CameraPrivacySwitchController::Get() {
  PrivacyHubController* privacy_hub_controller =
      Shell::Get()->privacy_hub_controller();
  return privacy_hub_controller ? privacy_hub_controller->camera_controller()
                                : nullptr;
}

void CameraPrivacySwitchController::OnCameraCountChanged(int new_camera_count) {
  camera_count_ = new_camera_count;
}

void CameraPrivacySwitchController::ActiveApplicationsChanged(
    bool application_added) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (application_added) {
    active_applications_using_camera_count_++;
  } else {
    DCHECK_GT(active_applications_using_camera_count_, 0);
    active_applications_using_camera_count_--;
  }

  const bool camera_muted_by_sw =
      GetUserSwitchPreference() == CameraSWPrivacySwitchSetting::kDisabled;

  auto* privacy_hub_notification_controller =
      PrivacyHubNotificationController::Get();
  CHECK(privacy_hub_notification_controller);

  if (!camera_muted_by_sw) {
    return;
  }

  if (features::IsVideoConferenceEnabled()) {
    // The `VideoConferenceTrayController` shows this info as a toast.
    return;
  }

  // NOTE: This logic mirrors the logic in
  // `MicrophonePrivacySwitchController`.
  if (active_applications_using_camera_count_ == 0) {
    // Always remove the notification when active applications go to 0.
    RemoveNotification();
  } else if (application_added) {
    if (InNotificationExtensionPeriod()) {
      // Notification is not updated. The extension period is prolonged.
      last_active_notification_update_time_ = base::Time::Now();
    } else {
      ShowNotification();
    }
    if (UsingCameraLEDFallback()) {
      ScheduleNotificationRemoval();
    }
  } else {
    // Application removed, update the notifications message.
    UpdateNotification();
    if (UsingCameraLEDFallback()) {
      ScheduleNotificationRemoval();
    }
  }
}

bool CameraPrivacySwitchController::UsingCameraLEDFallback() {
  auto* privacy_hub_controller = PrivacyHubController::Get();
  CHECK(privacy_hub_controller);
  return privacy_hub_controller->UsingCameraLEDFallback();
}

bool CameraPrivacySwitchController::IsCameraUsageAllowed() const {
  switch (GetUserSwitchPreference()) {
    case CameraSWPrivacySwitchSetting::kEnabled:
      return true;
    case CameraSWPrivacySwitchSetting::kDisabled:
      return false;
  }
}

void CameraPrivacySwitchController::ShowNotification() {
  last_active_notification_update_time_ = base::Time::Now();
  auto* privacy_hub_notification_controller =
      PrivacyHubNotificationController::Get();
  CHECK(privacy_hub_notification_controller);

  privacy_hub_notification_controller->ShowSoftwareSwitchNotification(
      SensorDisabledNotificationDelegate::Sensor::kCamera);
}

void CameraPrivacySwitchController::RemoveNotification() {
  if (InNotificationExtensionPeriod()) {
    // Do not remove notification within the extension period.
    return;
  }

  auto* privacy_hub_notification_controller =
      PrivacyHubNotificationController::Get();
  CHECK(privacy_hub_notification_controller);

  last_active_notification_update_time_ = base::Time::Min();
  privacy_hub_notification_controller->RemoveSoftwareSwitchNotification(
      SensorDisabledNotificationDelegate::Sensor::kCamera);
}

void CameraPrivacySwitchController::UpdateNotification() {
  auto* privacy_hub_notification_controller =
      PrivacyHubNotificationController::Get();
  CHECK(privacy_hub_notification_controller);

  last_active_notification_update_time_ = base::Time::Now();
  privacy_hub_notification_controller->UpdateSoftwareSwitchNotification(
      SensorDisabledNotificationDelegate::Sensor::kCamera);
}

void CameraPrivacySwitchController::ScheduleNotificationRemoval() {
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&CameraPrivacySwitchController::RemoveNotification,
                     weak_ptr_factory_.GetWeakPtr()),
      kCameraLedFallbackNotificationExtensionPeriod);
}

bool CameraPrivacySwitchController::InNotificationExtensionPeriod() {
  if (!UsingCameraLEDFallback()) {
    return false;
  }
  return base::Time::Now() < (last_active_notification_update_time_ +
                              kCameraLedFallbackNotificationExtensionPeriod);
}

}  // namespace ash