| // 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: |
| // Access restricted constructor. |
| ContentBlockObservationImpl(SessionController* session_controller, |
| SystemPermissionChangedCallback callback) |
| : callback_(std::move(callback)) { |
| // Microphone and camera settings are following the active user's |
| // preferences. Subscribing to the active user's pref changes. |
| CHECK(Shell::Get()->session_controller()); |
| active_user_pref_change_registrar_ = |
| std::make_unique<PrefChangeRegistrar>(); |
| active_user_pref_change_registrar_->Init( |
| Shell::Get()->session_controller()->GetActivePrefService()); |
| active_user_pref_change_registrar_->Add( |
| prefs::kUserCameraAllowed, |
| base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged, |
| base::Unretained(this))); |
| active_user_pref_change_registrar_->Add( |
| prefs::kUserMicrophoneAllowed, |
| base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged, |
| base::Unretained(this))); |
| |
| // Geolocation setting is exclusively controlled by the primary user of the |
| // session. This is different from the camera and microphone implementation. |
| // Subscribing to the primary user's pref changes. |
| primary_user_pref_change_registrar_ = |
| std::make_unique<PrefChangeRegistrar>(); |
| primary_user_pref_change_registrar_->Init( |
| Shell::Get()->session_controller()->GetPrimaryUserPrefService()); |
| primary_user_pref_change_registrar_->Add( |
| prefs::kUserGeolocationAccessLevel, |
| base::BindRepeating(&ContentBlockObservationImpl::OnPreferenceChanged, |
| base::Unretained(this))); |
| } |
| |
| ~ContentBlockObservationImpl() override = default; |
| |
| 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> active_user_pref_change_registrar_; |
| std::unique_ptr<PrefChangeRegistrar> primary_user_pref_change_registrar_; |
| 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; |
| } |
| |
| GeolocationAccessLevel GetSystemGeolocationAccessLevel() { |
| return GeolocationPrivacySwitchController::Get()->AccessLevel(); |
| } |
| |
| 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() { |
| auto default_sunrise_time = |
| base::Time::Now().LocalMidnight() + base::Hours(6); |
| SunRiseSetTime default_times = { |
| .sunrise = default_sunrise_time, |
| .sunset = default_sunrise_time + base::Hours(12)}; |
| |
| const ash::GeolocationController* geolocation_controller = |
| ash::GeolocationController::Get(); |
| const auto times = |
| geolocation_controller |
| ? geolocation_controller->GetSunRiseSetTime().value_or(default_times) |
| : default_times; |
| return std::make_pair(times.sunrise, times.sunset); |
| } |
| |
| 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)); |
| return observation; |
| } |
| |
| void OpenSystemSettings(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(); |
| } |
| } |
| |
| chrome::SettingsWindowManager::GetInstance()->ShowOSSettings( |
| ProfileManager::GetActiveUserProfile(), 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 |