chromium/ash/system/privacy_hub/privacy_hub_notification_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/privacy_hub_notification_controller.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/privacy_hub/camera_privacy_switch_controller.h"
#include "ash/system/privacy_hub/geolocation_privacy_switch_controller.h"
#include "ash/system/privacy_hub/microphone_privacy_switch_controller.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/sensor_disabled_notification_delegate.h"
#include "ash/system/system_notification_controller.h"
#include "base/containers/ring_buffer.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "components/prefs/pref_service.h"
#include "ui/message_center/message_center.h"

namespace ash {
using Sensor = SensorDisabledNotificationDelegate::Sensor;

namespace {

constexpr char kLearnMoreUrl[] =
    "https://support.google.com/chromebook/?p=privacy_hub";

void LogInvalidSensor(const Sensor sensor) {
  NOTREACHED() << "Invalid sensor: "
               << static_cast<std::underlying_type_t<Sensor>>(sensor);
}

// Throttler for geolocation notification. Limits notification to be displayed
// no more than once in an hour and no more that 3x in 24 hours.
class GeolocationThrottler : public PrivacyHubNotification::Throttler {
 public:
  // PrivacyHubNotification::Throttler
  bool ShouldThrottle() final {
    if (dismissals_.CurrentIndex() == 0) {
      // No dismissals recorded yet. -> No suppression.
      return false;
    }
    const base::TimeTicks current_time = base::TimeTicks::Now();
    const base::TimeDelta time_since_last_dismissal =
        current_time - **dismissals_.End();
    if (time_since_last_dismissal < base::Hours(1)) {
      // There has been a dismissal recorded within the last hour. ->
      // Notification suppressed.
      return true;
    }
    // Now we should suppress if there have been at least 3 accesses within last
    // 24 hours.
    if (dismissals_.CurrentIndex() < dismissals_.BufferSize()) {
      // Buffer is not full. -> There have not been 3 accesses. -> No
      // suppression.
      return false;
    }
    // There are 3 recorded accesses.
    const base::TimeDelta time_since_oldest_dismissal =
        current_time - **dismissals_.Begin();
    if (time_since_oldest_dismissal >= base::Hours(24)) {
      // The oldest one is older than 24 hours. -> No suppression.
      return false;
    }
    // All 3 must be within the 24 hours window. -> Notification suppressed.
    return true;
  }

  void RecordDismissalByUser() final {
    dismissals_.SaveToBuffer(base::TimeTicks::Now());
  }

 private:
  // Contains the times of up to the last three dismissals (in order).
  base::RingBuffer<base::TimeTicks, 3u> dismissals_;
};

}  // namespace

PrivacyHubNotificationController::PrivacyHubNotificationController() {
  PrivacyHubController* privacy_hub_controller = PrivacyHubController::Get();
  CHECK(privacy_hub_controller);

  // If privacy hub is on and the camera fallback mechanism is active, we need
  // to use a different set of messages.
  // TODO(b/289510726): remove when all cameras fully support the software
  // switch.
  // Note: if the privacy hub is not enabled, this object may still exist as it
  // is used by privacy indicators as well.

  std::vector<int> camera_messages;
  std::vector<int> combined_messages;

  if (privacy_hub_controller->UsingCameraLEDFallback()) {
    camera_messages = {
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_DISCLAIMER,
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME_WITH_DISCLAIMER,
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES_WITH_DISCLAIMER};
    combined_messages = {
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_DISCLAIMER,
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME_WITH_DISCLAIMER,
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES_WITH_DISCLAIMER};
  } else {
    camera_messages = {
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE,
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
        IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES};
    combined_messages = {
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE,
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
        IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES};
  }

  std::vector<int> microphone_messages = {
      IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE,
      IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
      IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES};

  auto camera_notification_descriptor = PrivacyHubNotificationDescriptor(
      SensorSet{Sensor::kCamera}, IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_TITLE,
      std::vector<int>{IDS_PRIVACY_HUB_TURN_ON_CAMERA_ACTION_BUTTON},
      camera_messages);

  auto microphone_notification_descriptor = PrivacyHubNotificationDescriptor(
      SensorSet{Sensor::kMicrophone},
      IDS_MICROPHONE_MUTED_BY_SW_SWITCH_NOTIFICATION_TITLE,
      std::vector<int>{IDS_MICROPHONE_MUTED_NOTIFICATION_ACTION_BUTTON},
      microphone_messages);

  auto combined_notification_descriptor = PrivacyHubNotificationDescriptor(
      SensorSet{Sensor::kCamera, Sensor::kMicrophone},
      IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_TITLE,
      std::vector<int>{
          IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_BUTTON,
          IDS_PRIVACY_HUB_OPEN_SETTINGS_PAGE_BUTTON},
      combined_messages);

  combined_notification_descriptor.delegate()->SetSecondButtonCallback(
      base::BindRepeating(
          &PrivacyHubNotificationController::OpenPrivacyHubSettingsPage));

  combined_notification_ = std::make_unique<PrivacyHubNotification>(
      kCombinedNotificationId, NotificationCatalogName::kPrivacyHubMicAndCamera,
      std::vector<PrivacyHubNotificationDescriptor>{
          camera_notification_descriptor, microphone_notification_descriptor,
          combined_notification_descriptor});

  microphone_hw_switch_notification_ = std::make_unique<PrivacyHubNotification>(
      kMicrophoneHardwareSwitchNotificationId,
      NotificationCatalogName::kMicrophoneMute,
      PrivacyHubNotificationDescriptor{
          SensorSet{Sensor::kMicrophone},
          IDS_MICROPHONE_MUTED_BY_HW_SWITCH_NOTIFICATION_TITLE,
          std::vector<int>{IDS_ASH_LEARN_MORE},
          std::vector<int>{
              IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE,
              IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
              IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES},
          base::MakeRefCounted<PrivacyHubNotificationClickDelegate>(
              base::BindRepeating(
                  PrivacyHubNotificationController::OpenSupportUrl,
                  Sensor::kMicrophone))});

  PrivacyHubNotificationDescriptor geolocation_notification_descriptor(
      SensorSet{Sensor::kLocation},
      IDS_PRIVACY_HUB_GEOLOCATION_OFF_NOTIFICATION_TITLE,
      std::vector<int>{IDS_PRIVACY_HUB_TURN_ON_GEOLOCATION_ACTION_BUTTON,
                       IDS_PRIVACY_HUB_TURN_ON_GEOLOCATION_LEARN_MORE_BUTTON},
      std::vector<int>{
          IDS_PRIVACY_HUB_GEOLOCATION_OFF_NOTIFICATION_MESSAGE,
          IDS_PRIVACY_HUB_GEOLOCATION_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
          IDS_PRIVACY_HUB_GEOLOCATION_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES});
  geolocation_notification_descriptor.delegate()->SetSecondButtonCallback(
      base::BindRepeating(PrivacyHubNotificationController::OpenSupportUrl,
                          Sensor::kLocation));
  geolocation_notification_ = std::make_unique<PrivacyHubNotification>(
      kGeolocationSwitchNotificationId,
      NotificationCatalogName::kGeolocationSwitch,
      std::move(geolocation_notification_descriptor));
  geolocation_notification_->SetThrottler(
      std::make_unique<GeolocationThrottler>());
}

PrivacyHubNotificationController::~PrivacyHubNotificationController() = default;

// static
PrivacyHubNotificationController* PrivacyHubNotificationController::Get() {
  SystemNotificationController* system_notification_controller =
      Shell::Get()->system_notification_controller();
  return system_notification_controller
             ? system_notification_controller->privacy_hub()
             : nullptr;
}

void PrivacyHubNotificationController::ShowSoftwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kMicrophone: {
      // Microphone software switch notification will be displayed now. If the
      // hardware switch notification is still not cleared, clear it first.
      microphone_hw_switch_notification_->Hide();
      [[fallthrough]];
    }
    case Sensor::kCamera: {
      AddSensor(sensor);
      combined_notification_->Show();
      break;
    }
    case Sensor::kLocation: {
      geolocation_notification_->Show();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

void PrivacyHubNotificationController::RemoveSoftwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kCamera: {
      [[fallthrough]];
    }
    case Sensor::kMicrophone: {
      RemoveSensor(sensor);
      if (!sensors_.empty()) {
        combined_notification_->Update();
      } else {
        combined_notification_->Hide();
      }
      break;
    }
    case Sensor::kLocation: {
      geolocation_notification_->Hide();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

void PrivacyHubNotificationController::UpdateSoftwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kCamera: {
      [[fallthrough]];
    }
    case Sensor::kMicrophone: {
      combined_notification_->Update();
      break;
    }
    case Sensor::kLocation: {
      geolocation_notification_->Update();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

// TODO(janlanik): Does this support geolocation?
bool PrivacyHubNotificationController::
    IsSoftwareSwitchNotificationDisplayedForSensor(Sensor sensor) {
  return combined_notification_->IsShown() && sensors_.Has(sensor);
}

void PrivacyHubNotificationController::
    SetPriorityForMicrophoneHardwareNotification(
        message_center::NotificationPriority priority) {
  microphone_hw_switch_notification_->SetPriority(priority);
}

void PrivacyHubNotificationController::ShowHardwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kMicrophone: {
      RemoveSensor(sensor);
      if (!sensors_.empty()) {
        combined_notification_->Update();
      } else {
        // As the hardware switch notification for microphone will be displayed
        // now, remove the sw switch notification.
        combined_notification_->Hide();
      }
      microphone_hw_switch_notification_->Show();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

void PrivacyHubNotificationController::RemoveHardwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kMicrophone: {
      microphone_hw_switch_notification_->Hide();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

void PrivacyHubNotificationController::UpdateHardwareSwitchNotification(
    const Sensor sensor) {
  switch (sensor) {
    case Sensor::kMicrophone: {
      microphone_hw_switch_notification_->Update();
      break;
    }
    default: {
      LogInvalidSensor(sensor);
      break;
    }
  }
}

void PrivacyHubNotificationController::OpenPrivacyHubSettingsPage() {
  privacy_hub_metrics::LogPrivacyHubOpenedFromNotification();
  Shell::Get()->system_tray_model()->client()->ShowPrivacyHubSettings();
}

void PrivacyHubNotificationController::OpenSupportUrl(Sensor sensor) {
  switch (sensor) {
    case Sensor::kMicrophone:
      privacy_hub_metrics::LogPrivacyHubLearnMorePageOpened(
          privacy_hub_metrics::PrivacyHubLearnMoreSensor::kMicrophone);
      break;
    case Sensor::kCamera:
      privacy_hub_metrics::LogPrivacyHubLearnMorePageOpened(
          privacy_hub_metrics::PrivacyHubLearnMoreSensor::kCamera);
      break;
    case Sensor::kLocation:
      privacy_hub_metrics::LogPrivacyHubLearnMorePageOpened(
          privacy_hub_metrics::PrivacyHubLearnMoreSensor::kGeolocation);
      return;
  }
  NewWindowDelegate::GetPrimary()->OpenUrl(
      GURL(kLearnMoreUrl), NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

// static
void PrivacyHubNotificationController::
    SetAndLogSensorPreferenceFromNotification(Sensor sensor,
                                              const bool enabled) {
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  if (!pref_service) {
    return;
  }

  switch (sensor) {
    case Sensor::kCamera: {
      pref_service->SetBoolean(prefs::kUserCameraAllowed, enabled);
      break;
    }
    case Sensor::kMicrophone: {
      pref_service->SetBoolean(prefs::kUserMicrophoneAllowed, enabled);
      break;
    }
    case Sensor::kLocation: {
      // Geolocation notification asks user to allow geolocation for everything
      // (not only system services).
      if (auto* controller = ash::GeolocationPrivacySwitchController::Get()) {
        controller->SetAccessLevel(enabled
                                       ? GeolocationAccessLevel::kAllowed
                                       : GeolocationAccessLevel::kDisallowed);
      }
      break;
    }
  }

  privacy_hub_metrics::LogSensorEnabledFromNotification(sensor, enabled);
}

std::unique_ptr<SensorDisabledNotificationDelegate>
PrivacyHubNotificationController::SetSensorDisabledNotificationDelegate(
    std::unique_ptr<SensorDisabledNotificationDelegate> delegate) {
  std::swap(sensor_disabled_notification_delegate_, delegate);
  return delegate;
}

SensorDisabledNotificationDelegate*
PrivacyHubNotificationController::sensor_disabled_notification_delegate() {
  SensorDisabledNotificationDelegate* delegate =
      sensor_disabled_notification_delegate_.get();
  CHECK(delegate);
  return delegate;
}

void PrivacyHubNotificationController::AddSensor(Sensor sensor) {
  sensors_.Put(sensor);
  combined_notification_->SetSensors(sensors_);
}

void PrivacyHubNotificationController::RemoveSensor(Sensor sensor) {
  sensors_.Remove(sensor);
  combined_notification_->SetSensors(sensors_);
}

}  // namespace ash