chromium/ash/system/privacy/privacy_indicators_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/privacy_indicators_controller.h"

#include <string>

#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.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_tray_item_view.h"
#include "ash/system/status_area_widget.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "media/capture/video/chromeos/camera_hal_dispatcher_impl.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"

namespace ash {

namespace {

PrivacyIndicatorsController* g_controller_instance = nullptr;

// Create a notification with the customized metadata for privacy indicators.
std::unique_ptr<message_center::Notification>
CreatePrivacyIndicatorsNotification(
    const std::string& app_id,
    std::optional<std::u16string> app_name,
    bool is_camera_used,
    bool is_microphone_used,
    scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate) {
  std::u16string app_name_str = app_name.value_or(l10n_util::GetStringUTF16(
      IDS_PRIVACY_NOTIFICATION_MESSAGE_DEFAULT_APP_NAME));

  std::u16string title;
  std::u16string message;
  const gfx::VectorIcon* app_icon;
  if (is_camera_used && is_microphone_used) {
    title = l10n_util::GetStringFUTF16(
        IDS_PRIVACY_NOTIFICATION_TITLE_CAMERA_AND_MIC, app_name_str);
    app_icon = &kPrivacyIndicatorsIcon;
  } else if (is_camera_used) {
    title = l10n_util::GetStringFUTF16(IDS_PRIVACY_NOTIFICATION_TITLE_CAMERA,
                                       app_name_str);
    app_icon = &kPrivacyIndicatorsCameraIcon;
  } else {
    title = l10n_util::GetStringFUTF16(IDS_PRIVACY_NOTIFICATION_TITLE_MIC,
                                       app_name_str);
    app_icon = &kPrivacyIndicatorsMicrophoneIcon;
  }

  message_center::RichNotificationData optional_fields;
  optional_fields.pinned = true;
  // Make the notification low priority so that it is silently added (no popup).
  optional_fields.priority = message_center::LOW_PRIORITY;

  optional_fields.parent_vector_small_image = &kPrivacyIndicatorsIcon;

  if (delegate->launch_settings_callback()) {
    optional_fields.buttons.emplace_back(
        features::AreOngoingProcessesEnabled()
            ? message_center::ButtonInfo(
                  /*vector_icon=*/&kSettingsIcon,
                  /*accessible_name=*/l10n_util::GetStringUTF16(
                      IDS_PRIVACY_NOTIFICATION_BUTTON_APP_SETTINGS))
            : message_center::ButtonInfo(l10n_util::GetStringUTF16(
                  IDS_PRIVACY_NOTIFICATION_BUTTON_APP_SETTINGS)));
  }

  auto notification = CreateSystemNotificationPtr(
      message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE,
      GetPrivacyIndicatorsNotificationId(app_id), title, std::u16string(),
      /*display_source=*/std::u16string(),
      /*origin_url=*/GURL(),
      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
                                 kPrivacyIndicatorsNotifierId,
                                 NotificationCatalogName::kPrivacyIndicators),
      optional_fields,
      /*delegate=*/delegate, *app_icon,
      message_center::SystemNotificationWarningLevel::NORMAL);

  notification->set_accent_color_id(ui::kColorAshPrivacyIndicatorsBackground);

  return notification;
}

// Adds, updates, or removes the privacy notification associated with the given
// `app_id`.
void ModifyPrivacyIndicatorsNotification(
    const std::string& app_id,
    std::optional<std::u16string> app_name,
    bool is_camera_used,
    bool is_microphone_used,
    scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate) {
  if (features::IsVideoConferenceEnabled()) {
    return;
  }

  auto* message_center = message_center::MessageCenter::Get();
  std::string id = GetPrivacyIndicatorsNotificationId(app_id);
  bool notification_exists = message_center->FindNotificationById(id);

  if (!is_camera_used && !is_microphone_used) {
    if (notification_exists)
      message_center->RemoveNotification(id, /*by_user=*/false);
    return;
  }

  auto notification = CreatePrivacyIndicatorsNotification(
      app_id, app_name, is_camera_used, is_microphone_used, delegate);
  if (notification_exists) {
    message_center->UpdateNotification(id, std::move(notification));
    return;
  }
  message_center->AddNotification(std::move(notification));
}

// Updates the `PrivacyIndicatorsTrayItemView` across all status area widgets.
void UpdatePrivacyIndicatorsView(bool is_camera_used,
                                 bool is_microphone_used,
                                 bool is_new_app,
                                 bool was_camera_in_use,
                                 bool was_microphone_in_use) {
  if (features::IsVideoConferenceEnabled()) {
    return;
  }

  DCHECK(Shell::HasInstance());
  for (auto* root_window_controller :
       Shell::Get()->GetAllRootWindowControllers()) {
    DCHECK(root_window_controller);
    auto* status_area_widget = root_window_controller->GetStatusAreaWidget();
    DCHECK(status_area_widget);

    auto* privacy_indicators_view =
        status_area_widget->notification_center_tray()
            ->privacy_indicators_view();

    DCHECK(privacy_indicators_view);
    privacy_indicators_view->OnCameraAndMicrophoneAccessStateChanged(
        is_camera_used, is_microphone_used, is_new_app, was_camera_in_use,
        was_microphone_in_use);
  }
}

// Updates the access status of `app_id` for the given `access_map`.
void UpdateAccessStatus(
    const std::string& app_id,
    bool is_accessed,
    std::map<std::string, ash::PrivacyIndicatorsAppInfo>& access_map,
    std::optional<std::u16string> app_name,
    scoped_refptr<ash::PrivacyIndicatorsNotificationDelegate> delegate) {
  if (access_map.contains(app_id) == is_accessed) {
    return;
  }

  if (is_accessed) {
    ash::PrivacyIndicatorsAppInfo info;
    info.app_name = app_name;
    info.delegate = delegate;
    access_map[app_id] = std::move(info);
  } else {
    access_map.erase(app_id);
  }
}

void UpdatePrivacyIndicatorsVisibility() {
  DCHECK(Shell::HasInstance());
  for (auto* root_window_controller :
       Shell::Get()->GetAllRootWindowControllers()) {
    CHECK(root_window_controller);
    auto* status_area_widget = root_window_controller->GetStatusAreaWidget();
    CHECK(status_area_widget);

    auto* privacy_indicators_view =
        status_area_widget->notification_center_tray()
            ->privacy_indicators_view();
    CHECK(privacy_indicators_view);

    privacy_indicators_view->UpdateVisibility();
  }
}

}  // namespace

PrivacyIndicatorsNotificationDelegate::PrivacyIndicatorsNotificationDelegate(
    std::optional<base::RepeatingClosure> launch_settings_callback)
    : launch_settings_callback_(launch_settings_callback) {}

PrivacyIndicatorsNotificationDelegate::
    ~PrivacyIndicatorsNotificationDelegate() = default;

void PrivacyIndicatorsNotificationDelegate::SetLaunchSettingsCallback(
    const base::RepeatingClosure& launch_settings_callback) {
  launch_settings_callback_ = launch_settings_callback;
}

void PrivacyIndicatorsNotificationDelegate::Click(
    const std::optional<int>& button_index,
    const std::optional<std::u16string>& reply) {
  if (!button_index) {
    // Click on the notification body should launch app settings if possible.
    if (launch_settings_callback_) {
      launch_settings_callback_->Run();
    }
    return;
  }

  CHECK(*button_index == 0);
  if (launch_settings_callback_) {
    launch_settings_callback_->Run();
  }
}

std::string GetPrivacyIndicatorsNotificationId(const std::string& app_id) {
  return kPrivacyIndicatorsNotificationIdPrefix + app_id;
}

PrivacyIndicatorsAppInfo::PrivacyIndicatorsAppInfo() = default;

PrivacyIndicatorsAppInfo::~PrivacyIndicatorsAppInfo() = default;

PrivacyIndicatorsController::PrivacyIndicatorsController() {
  DCHECK(!g_controller_instance);
  g_controller_instance = this;

  CrasAudioHandler::Get()->AddAudioObserver(this);
  media::CameraHalDispatcherImpl::GetInstance()->AddCameraPrivacySwitchObserver(
      this);
}

PrivacyIndicatorsController::~PrivacyIndicatorsController() {
  DCHECK_EQ(this, g_controller_instance);
  g_controller_instance = nullptr;

  CrasAudioHandler::Get()->RemoveAudioObserver(this);
  media::CameraHalDispatcherImpl::GetInstance()
      ->RemoveCameraPrivacySwitchObserver(this);
}

// static
PrivacyIndicatorsController* PrivacyIndicatorsController::Get() {
  return g_controller_instance;
}

void PrivacyIndicatorsController::UpdatePrivacyIndicators(
    const std::string& app_id,
    std::optional<std::u16string> app_name,
    bool is_camera_used,
    bool is_microphone_used,
    scoped_refptr<PrivacyIndicatorsNotificationDelegate> delegate,
    PrivacyIndicatorsSource source) {
  const bool is_new_app = !apps_using_camera_.contains(app_id) &&
                          !apps_using_microphone_.contains(app_id);
  const bool was_camera_in_use = IsCameraUsed();
  const bool was_microphone_in_use = IsMicrophoneUsed();

  UpdateAccessStatus(app_id, /*is_accessed=*/is_camera_used,
                     /*access_map=*/apps_using_camera_, app_name, delegate);
  UpdateAccessStatus(app_id,
                     /*is_accessed=*/is_microphone_used,
                     /*access_map=*/apps_using_microphone_, app_name, delegate);

  is_camera_used = is_camera_used && !camera_muted_by_hardware_switch_ &&
                   !camera_muted_by_software_switch_;
  is_microphone_used =
      is_microphone_used && !CrasAudioHandler::Get()->IsInputMuted();

  ModifyPrivacyIndicatorsNotification(app_id, app_name, is_camera_used,
                                      is_microphone_used, delegate);
  UpdatePrivacyIndicatorsView(is_camera_used, is_microphone_used, is_new_app,
                              was_camera_in_use, was_microphone_in_use);

  base::UmaHistogramEnumeration("Ash.PrivacyIndicators.Source", source);
}

void PrivacyIndicatorsController::OnCameraHWPrivacySwitchStateChanged(
    const std::string& device_id,
    cros::mojom::CameraPrivacySwitchState state) {
  camera_muted_by_hardware_switch_ =
      state == cros::mojom::CameraPrivacySwitchState::ON;

  UpdateForCameraMuteStateChanged();
}

void PrivacyIndicatorsController::OnCameraSWPrivacySwitchStateChanged(
    cros::mojom::CameraPrivacySwitchState state) {
  camera_muted_by_software_switch_ =
      state == cros::mojom::CameraPrivacySwitchState::ON;

  UpdateForCameraMuteStateChanged();
}

void PrivacyIndicatorsController::OnInputMuteChanged(
    bool mute_on,
    CrasAudioHandler::InputMuteChangeMethod method) {
  // Iterate through all the apps that are tracked as using the microphone, then
  // modify the notification according to the mute state of the microphone.
  for (const auto& [app_id, app_info] : apps_using_microphone_) {
    // Retrieve camera usage state for each individual app to update in the
    // notification.
    bool is_camera_used = apps_using_camera_.contains(app_id) &&
                          !camera_muted_by_hardware_switch_ &&
                          !camera_muted_by_software_switch_;
    ModifyPrivacyIndicatorsNotification(
        app_id, app_info.app_name, is_camera_used,
        /*is_microphone_used=*/!mute_on, app_info.delegate);
  }

  UpdatePrivacyIndicatorsVisibility();
}

void PrivacyIndicatorsController::UpdateForCameraMuteStateChanged() {
  // Iterate through all the apps that are tracked as using the camera, then
  // modify the notification according to the mute state of camera.
  for (const auto& [app_id, app_info] : apps_using_camera_) {
    // Retrieve microphone usage state for each individual app to update in the
    // notification.
    bool is_camera_used =
        !camera_muted_by_hardware_switch_ && !camera_muted_by_software_switch_;
    bool is_microphone_used = apps_using_microphone_.contains(app_id) &&
                              !CrasAudioHandler::Get()->IsInputMuted();
    ModifyPrivacyIndicatorsNotification(app_id, app_info.app_name,
                                        is_camera_used, is_microphone_used,
                                        app_info.delegate);
  }

  UpdatePrivacyIndicatorsVisibility();
}

bool PrivacyIndicatorsController::IsCameraUsed() const {
  return !apps_using_camera_.empty() && !camera_muted_by_hardware_switch_ &&
         !camera_muted_by_software_switch_;
}

bool PrivacyIndicatorsController::IsMicrophoneUsed() const {
  return !apps_using_microphone_.empty() &&
         !CrasAudioHandler::Get()->IsInputMuted();
}

void UpdatePrivacyIndicatorsScreenShareStatus(bool is_screen_sharing) {
  if (features::IsVideoConferenceEnabled()) {
    return;
  }

  DCHECK(Shell::HasInstance());
  for (auto* root_window_controller :
       Shell::Get()->GetAllRootWindowControllers()) {
    DCHECK(root_window_controller);
    auto* status_area_widget = root_window_controller->GetStatusAreaWidget();
    DCHECK(status_area_widget);

    auto* privacy_indicators_view =
        status_area_widget->notification_center_tray()
            ->privacy_indicators_view();

    DCHECK(privacy_indicators_view);

    privacy_indicators_view->UpdateScreenShareStatus(is_screen_sharing);
  }
}

}  // namespace ash