chromium/chrome/browser/ash/privacy_hub/privacy_hub_util.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 "chrome/browser/ash/privacy_hub/privacy_hub_util.h"

#include <optional>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/privacy_hub_delegate.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/geolocation/geolocation_controller.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_notification_controller.h"
#include "ash/system/privacy_hub/sensor_disabled_notification_delegate.h"
#include "ash/webui/settings/public/constants/routes.mojom-forward.h"
#include "base/check_deref.h"
#include "base/functional/callback.h"
#include "base/notreached.h"
#include "base/supports_user_data.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/app_access/app_access_notifier.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chromeos/ash/components/camera_presence_notifier/camera_presence_notifier.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"

namespace ash::privacy_hub_util {

namespace {

PrefService* ActivePrefService() {
  auto* session_controller =
      CHECK_DEREF(ash::Shell::Get()).session_controller();
  return CHECK_DEREF(session_controller).GetActivePrefService();
}

ScopedUserPermissionPrefForTest::AccessLevel GetCurrentAccessLevel(
    ContentType type) {
  if (type == ContentType::MEDIASTREAM_CAMERA) {
    const bool allowed =
        CHECK_DEREF(ActivePrefService()).GetBoolean(prefs::kUserCameraAllowed);
    return allowed ? ScopedUserPermissionPrefForTest::AccessLevel::kAllowed
                   : ScopedUserPermissionPrefForTest::AccessLevel::kDisallowed;
  }
  if (type == ContentType::MEDIASTREAM_MIC) {
    const bool allowed = CHECK_DEREF(ActivePrefService())
                             .GetBoolean(prefs::kUserMicrophoneAllowed);
    return allowed ? ScopedUserPermissionPrefForTest::AccessLevel::kAllowed
                   : ScopedUserPermissionPrefForTest::AccessLevel::kDisallowed;
  }
  CHECK(type == ContentType::GEOLOCATION);
  return static_cast<ScopedUserPermissionPrefForTest::AccessLevel>(
      CHECK_DEREF(ActivePrefService())
          .GetInteger(prefs::kUserGeolocationAccessLevel));
}

void SetCurrentAccessLevel(
    ContentType type,
    ScopedUserPermissionPrefForTest::AccessLevel access_level) {
  if (type == ContentType::MEDIASTREAM_CAMERA) {
    CHECK(access_level !=
          ScopedUserPermissionPrefForTest::AccessLevel::kOnlyAllowedForSystem);
    CHECK_DEREF(ActivePrefService())
        .SetBoolean(prefs::kUserCameraAllowed,
                    access_level ==
                        ScopedUserPermissionPrefForTest::AccessLevel::kAllowed);
    return;
  }
  if (type == ContentType::MEDIASTREAM_MIC) {
    CHECK(access_level !=
          ScopedUserPermissionPrefForTest::AccessLevel::kOnlyAllowedForSystem);
    CHECK_DEREF(ActivePrefService())
        .SetBoolean(prefs::kUserMicrophoneAllowed,
                    access_level ==
                        ScopedUserPermissionPrefForTest::AccessLevel::kAllowed);
    return;
  }
  CHECK(type == ContentType::GEOLOCATION);
  CHECK_DEREF(ActivePrefService())
      .SetInteger(prefs::kUserGeolocationAccessLevel,
                  static_cast<int>(access_level));
}

class ContentBlockObservationImpl : public ContentBlockObservation,
                                    public SessionObserver {
 public:
  // Access restricted constructor.
  ContentBlockObservationImpl(SessionController* session_controller,
                              SystemPermissionChangedCallback callback)
      : callback_(std::move(callback)), session_observation_(this) {
    session_observation_.Observe(session_controller);
  }

  ~ContentBlockObservationImpl() override = default;

  // SessionObserver:
  void OnActiveUserPrefServiceChanged(PrefService* pref_service) override {
    // Subscribing to pref changes.
    pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
    pref_change_registrar_->Init(pref_service);
    pref_change_registrar_->Add(
        prefs::kUserCameraAllowed,
        base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged,
                            base::Unretained(this)));
    pref_change_registrar_->Add(
        prefs::kUserMicrophoneAllowed,
        base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged,
                            base::Unretained(this)));
    pref_change_registrar_->Add(
        prefs::kUserGeolocationAccessLevel,
        base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged,
                            base::Unretained(this)));
  }
  void OnChromeTerminating() override { session_observation_.Reset(); }

 private:
  // Handles changes in the user pref ( e.g. toggling the camera switch on
  // Privacy Hub UI).
  void OnPreferenceChanged(const std::string& pref_name) {
    if (pref_name == prefs::kUserCameraAllowed) {
      callback_.Run(
          ContentType::MEDIASTREAM_CAMERA,
          privacy_hub_util::ContentBlocked(ContentType::MEDIASTREAM_CAMERA));
      return;
    }
    if (pref_name == prefs::kUserMicrophoneAllowed) {
      callback_.Run(
          ContentType::MEDIASTREAM_MIC,
          privacy_hub_util::ContentBlocked(ContentType::MEDIASTREAM_MIC));
      return;
    }
    if (pref_name == prefs::kUserGeolocationAccessLevel) {
      callback_.Run(ContentType::GEOLOCATION,
                    privacy_hub_util::ContentBlocked(ContentType::GEOLOCATION));
      return;
    }
    NOTREACHED();
  }

  SystemPermissionChangedCallback callback_;
  std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;
  base::ScopedObservation<SessionController, ContentBlockObservationImpl>
      session_observation_;
  base::WeakPtrFactory<ContentBlockObservationImpl> weak_ptr_factory_{this};
};
}  // namespace

void SetFrontend(PrivacyHubDelegate* ptr) {
  PrivacyHubController* const controller = PrivacyHubController::Get();
  if (controller != nullptr) {
    // Controller may not be available when used from a test.
    controller->SetFrontend(ptr);
  }
}

bool MicrophoneSwitchState() {
  return ui::MicrophoneMuteSwitchMonitor::Get()->microphone_mute_switch_on();
}

bool ShouldForceDisableCameraSwitch() {
  PrivacyHubController* controller = PrivacyHubController::Get();
  if (!controller || !controller->camera_controller()) {
    return false;
  }
  return controller->camera_controller()->IsCameraAccessForceDisabled();
}

void SetUpCameraCountObserver() {
  auto* camera_controller = CameraPrivacySwitchController::Get();
  CHECK(camera_controller);

  base::RepeatingCallback<void(int)> update_camera_count_in_privacy_hub =
      base::BindRepeating(
          [](CameraPrivacySwitchController* controller, int camera_count) {
            controller->OnCameraCountChanged(camera_count);
          },
          camera_controller);
  auto notifier = std::make_unique<CameraPresenceNotifier>(
      std::move(update_camera_count_in_privacy_hub));
  notifier->Start();

  static const char kUserDataKey = '\0';
  camera_controller->SetUserData(&kUserDataKey, std::move(notifier));
}

// Notifies the Privacy Hub controller.
void TrackGeolocationAttempted(const std::string& name) {
  if (!features::IsCrosPrivacyHubLocationEnabled()) {
    return;
  }
  GeolocationPrivacySwitchController* controller =
      GeolocationPrivacySwitchController::Get();
  // TODO(b/288854399): Remove this if.
  if (controller) {
    controller->TrackGeolocationAttempted(name);
  }
}

// Notifies the Privacy Hub controller.
void TrackGeolocationRelinquished(const std::string& name) {
  if (!features::IsCrosPrivacyHubLocationEnabled()) {
    return;
  }
  GeolocationPrivacySwitchController* controller =
      GeolocationPrivacySwitchController::Get();
  // TODO(b/288854399): Remove this if.
  if (controller) {
    controller->TrackGeolocationRelinquished(name);
  }
}

bool IsCrosLocationOobeNegotiationNeeded() {
  // No negotiation needed, if the PH Location feature is not yet enabled.
  if (!ash::features::IsCrosPrivacyHubLocationEnabled()) {
    return false;
  }

  const Profile* profile = ProfileManager::GetPrimaryUserProfile();
  if (profile->GetPrefs()->IsManagedPreference(
          ash::prefs::kUserGeolocationAccessLevel)) {
    return false;
  }

  return true;
}

namespace {
std::optional<bool> camera_led_fallback_for_testing{};
}

// TODO(b/289510726): remove when all cameras fully support the software
// switch.
bool UsingCameraLEDFallback() {
  if (!camera_led_fallback_for_testing.has_value()) {
    CameraPrivacySwitchController* const controller =
        CameraPrivacySwitchController::Get();
    CHECK(controller);
    return controller->UsingCameraLEDFallback();
  }

  // Can happen in some testing environments
  CHECK(camera_led_fallback_for_testing.has_value());
  return camera_led_fallback_for_testing.value();
}

ScopedCameraLedFallbackForTesting::ScopedCameraLedFallbackForTesting(
    bool value) {
  CHECK(!camera_led_fallback_for_testing.has_value());
  camera_led_fallback_for_testing = value;
}

ScopedCameraLedFallbackForTesting::~ScopedCameraLedFallbackForTesting() {
  CHECK(camera_led_fallback_for_testing.has_value());
  camera_led_fallback_for_testing.reset();
  CHECK(!camera_led_fallback_for_testing.has_value());
}

void SetAppAccessNotifier(AppAccessNotifier* app_access_notifier) {
  // Wraps the `AppAccessNotifier` to be used from
  // `PrivacyHubNotificationController`.
  class Wrapper : public SensorDisabledNotificationDelegate {
   public:
    explicit Wrapper(AppAccessNotifier* notifier)
        : notifier_(raw_ref<AppAccessNotifier>::from_ptr(notifier)) {}

    std::vector<std::u16string> GetAppsAccessingSensor(Sensor sensor) override {
      switch (sensor) {
        case Sensor::kCamera:
          return notifier_->GetAppsAccessingCamera();
        case Sensor::kMicrophone:
          return notifier_->GetAppsAccessingMicrophone();
        case Sensor::kLocation:
          break;
      }
      NOTREACHED();
    }

   private:
    raw_ref<AppAccessNotifier> notifier_;
  };

  PrivacyHubNotificationController* controller =
      PrivacyHubNotificationController::Get();
  CHECK(controller);
  controller->SetSensorDisabledNotificationDelegate(
      app_access_notifier ? std::make_unique<Wrapper>(app_access_notifier)
                          : nullptr);
}

std::pair<base::Time, base::Time> SunriseSunsetSchedule() {
  const base::Time default_sunrise_time =
      base::Time::Now().LocalMidnight() + base::Hours(6);
  const base::Time default_sunset_time = default_sunrise_time + base::Hours(12);
  const ash::GeolocationController* geolocation_controller =
      ash::GeolocationController::Get();
  const base::Time sunrise_time =
      geolocation_controller
          ? geolocation_controller->GetSunriseTime().value_or(
                default_sunrise_time)
          : default_sunrise_time;
  const base::Time sunset_time =
      geolocation_controller ? geolocation_controller->GetSunsetTime().value_or(
                                   default_sunset_time)
                             : default_sunrise_time;
  return std::make_pair(sunrise_time, sunset_time);
}

bool ContentBlocked(ContentType type) {
  switch (type) {
    case ContentType::MEDIASTREAM_CAMERA: {
      auto* const controller = CameraPrivacySwitchController::Get();
      CHECK(controller);
      return !controller->IsCameraUsageAllowed();
    }
    case ContentType::MEDIASTREAM_MIC: {
      auto* const controller = MicrophonePrivacySwitchController::Get();
      CHECK(controller);
      return !controller->IsMicrophoneUsageAllowed();
    }
    case ContentType::GEOLOCATION: {
      if (!features::IsCrosPrivacyHubLocationEnabled()) {
        return false;  // content not blocked as geo blocking not supported.
      }
      auto* const controller = GeolocationPrivacySwitchController::Get();
      CHECK(controller);
      return !controller->IsGeolocationUsageAllowedForApps();
    }
    default: {
      // If the provided content type is not controllable in ChromeOS, then it
      // is not blocked.
      return false;
    }
  }
}

std::unique_ptr<ContentBlockObservation> CreateObservationForBlockedContent(
    SystemPermissionChangedCallback callback) {
  if (!ash::Shell::HasInstance()) {
    return nullptr;
  }
  auto* shell = ash::Shell::Get();
  CHECK(shell);
  auto* session_controller = shell->session_controller();
  if (!session_controller) {
    return nullptr;
  }
  PrefService* pref_service =
      session_controller->GetLastActiveUserPrefService();
  if (!pref_service) {
    return nullptr;
  }

  auto observation = std::make_unique<ContentBlockObservationImpl>(
      session_controller, std::move(callback));
  observation->OnActiveUserPrefServiceChanged(pref_service);
  return observation;
}

void OpenSystemSettings(Profile* profile, ContentType type) {
  const char* settings_path = "";
  switch (type) {
    case ContentType::MEDIASTREAM_CAMERA: {
      settings_path = chromeos::settings::mojom::kPrivacyHubCameraSubpagePath;
      break;
    }
    case ContentType::MEDIASTREAM_MIC: {
      settings_path =
          chromeos::settings::mojom::kPrivacyHubMicrophoneSubpagePath;
      break;
    }
    case ContentType::GEOLOCATION: {
      settings_path =
          chromeos::settings::mojom::kPrivacyHubGeolocationSubpagePath;
      break;
    }
    default: {
      // This should only be called for camera, microphone, or geolocation.
      NOTREACHED_IN_MIGRATION();
      return;
    }
  }

  chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile,
                                                               settings_path);
}

ScopedUserPermissionPrefForTest::ScopedUserPermissionPrefForTest(
    ContentType type,
    ScopedUserPermissionPrefForTest::AccessLevel access_level)
    : content_type_(type), previous_access_level_(GetCurrentAccessLevel(type)) {
  // Only Geolocation, camera and mic are supported.
  CHECK((ContentType::GEOLOCATION == type) ||
        (ContentType::MEDIASTREAM_CAMERA == type) ||
        (ContentType::MEDIASTREAM_MIC == type));
  if (ContentType::GEOLOCATION != type) {
    CHECK(access_level !=
          ScopedUserPermissionPrefForTest::AccessLevel::kOnlyAllowedForSystem);
  }
  SetCurrentAccessLevel(type, access_level);
}

ScopedUserPermissionPrefForTest::~ScopedUserPermissionPrefForTest() {
  SetCurrentAccessLevel(content_type_, previous_access_level_);
}

}  // namespace ash::privacy_hub_util