// Copyright 2021 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/crosapi/prefs_ash.h"
#include <string>
#include <string_view>
#include <utility>
#include "ash/constants/ash_pref_names.h"
#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/functional/bind.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/lifetime/termination_notification.h"
#include "chrome/browser/media/router/discovery/access_code/access_code_cast_feature.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_prefs.h"
#include "chromeos/crosapi/mojom/prefs.mojom.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/language/core/browser/pref_names.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/search_engines/default_search_manager.h"
#include "components/user_manager/user_manager.h"
namespace crosapi {
namespace {
// List of all mojom::PrefPaths associated with profile prefs, and their
// corresponding paths in the prefstore.
std::string_view GetProfilePrefNameForPref(mojom::PrefPath path) {
static constexpr auto kProfilePrefPathToName =
base::MakeFixedFlatMap<mojom::PrefPath, std::string_view>({
{mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled,
ash::prefs::kAccessibilitySpokenFeedbackEnabled},
{mojom::PrefPath::kAccessibilityReducedAnimationsEnabled,
ash::prefs::kAccessibilityReducedAnimationsEnabled},
{mojom::PrefPath::kUserGeolocationAccessLevel,
ash::prefs::kUserGeolocationAccessLevel},
{mojom::PrefPath::kQuickAnswersEnabled,
quick_answers::prefs::kQuickAnswersEnabled},
{mojom::PrefPath::kQuickAnswersConsentStatus,
quick_answers::prefs::kQuickAnswersConsentStatus},
{mojom::PrefPath::kQuickAnswersDefinitionEnabled,
quick_answers::prefs::kQuickAnswersDefinitionEnabled},
{mojom::PrefPath::kQuickAnswersTranslationEnabled,
quick_answers::prefs::kQuickAnswersTranslationEnabled},
{mojom::PrefPath::kQuickAnswersUnitConversionEnabled,
quick_answers::prefs::kQuickAnswersUnitConversionEnabled},
{mojom::PrefPath::kQuickAnswersNoticeImpressionCount,
quick_answers::prefs::kQuickAnswersNoticeImpressionCount},
{mojom::PrefPath::kQuickAnswersNoticeImpressionDuration,
quick_answers::prefs::kQuickAnswersNoticeImpressionDuration},
{mojom::PrefPath::kPreferredLanguages,
language::prefs::kPreferredLanguages},
{mojom::PrefPath::kApplicationLocale,
language::prefs::kApplicationLocale},
{mojom::PrefPath::kSharedStorage, prefs::kSharedStorage},
{mojom::PrefPath::kMultitaskMenuNudgeClamshellShownCount,
ash::prefs::kMultitaskMenuNudgeClamshellShownCount},
{mojom::PrefPath::kMultitaskMenuNudgeClamshellLastShown,
ash::prefs::kMultitaskMenuNudgeClamshellLastShown},
{mojom::PrefPath::kAccessCodeCastDevices,
media_router::prefs::kAccessCodeCastDevices},
{mojom::PrefPath::kAccessCodeCastDeviceAdditionTime,
media_router::prefs::kAccessCodeCastDeviceAdditionTime},
{mojom::PrefPath::kDefaultSearchProviderDataPrefName,
DefaultSearchManager::kDefaultSearchProviderDataPrefName},
{mojom::PrefPath::kIsolatedWebAppsEnabled,
ash::prefs::kIsolatedWebAppsEnabled},
{mojom::PrefPath::kHmrEnabled, ash::prefs::kHmrEnabled},
{mojom::PrefPath::kUserCameraAllowed, ash::prefs::kUserCameraAllowed},
{mojom::PrefPath::kUserMicrophoneAllowed,
ash::prefs::kUserMicrophoneAllowed},
{mojom::PrefPath::kHMRConsentStatus, ash::prefs::kHMRConsentStatus},
{mojom::PrefPath::kHMRConsentWindowDismissCount,
ash::prefs::kHMRConsentWindowDismissCount},
});
auto pref_name = kProfilePrefPathToName.find(path);
DCHECK(pref_name != kProfilePrefPathToName.end());
return pref_name->second;
}
// List of all mojom::PrefPaths associated with extension controlled prefs,
// and their corresponding paths in the prefstore.
std::string_view GetExtensionPrefNameForPref(mojom::PrefPath path) {
static constexpr auto kExtensionPrefPathToName =
base::MakeFixedFlatMap<mojom::PrefPath, std::string_view>(
{{mojom::PrefPath::kDockedMagnifierEnabled,
ash::prefs::kDockedMagnifierEnabled},
{mojom::PrefPath::kAccessibilityAutoclickEnabled,
ash::prefs::kAccessibilityAutoclickEnabled},
{mojom::PrefPath::kAccessibilityCaretHighlightEnabled,
ash::prefs::kAccessibilityCaretHighlightEnabled},
{mojom::PrefPath::kAccessibilityCursorColorEnabled,
ash::prefs::kAccessibilityCursorColorEnabled},
{mojom::PrefPath::kAccessibilityCursorHighlightEnabled,
ash::prefs::kAccessibilityCursorHighlightEnabled},
{mojom::PrefPath::kAccessibilityDictationEnabled,
ash::prefs::kAccessibilityDictationEnabled},
{mojom::PrefPath::kAccessibilityFocusHighlightEnabled,
ash::prefs::kAccessibilityFocusHighlightEnabled},
{mojom::PrefPath::kAccessibilityHighContrastEnabled,
ash::prefs::kAccessibilityHighContrastEnabled},
{mojom::PrefPath::kAccessibilityLargeCursorEnabled,
ash::prefs::kAccessibilityLargeCursorEnabled},
{mojom::PrefPath::kAccessibilityScreenMagnifierEnabled,
ash::prefs::kAccessibilityScreenMagnifierEnabled},
{mojom::PrefPath::kAccessibilitySelectToSpeakEnabled,
ash::prefs::kAccessibilitySelectToSpeakEnabled},
{mojom::PrefPath::kExtensionAccessibilitySpokenFeedbackEnabled,
ash::prefs::kAccessibilitySpokenFeedbackEnabled},
{mojom::PrefPath::kAccessibilityStickyKeysEnabled,
ash::prefs::kAccessibilityStickyKeysEnabled},
{mojom::PrefPath::kAccessibilitySwitchAccessEnabled,
ash::prefs::kAccessibilitySwitchAccessEnabled},
{mojom::PrefPath::kAccessibilityVirtualKeyboardEnabled,
ash::prefs::kAccessibilityVirtualKeyboardEnabled},
{mojom::PrefPath::kProxy, ash::prefs::kProxy}});
auto pref_name = kExtensionPrefPathToName.find(path);
DCHECK(pref_name != kExtensionPrefPathToName.end());
return pref_name->second;
}
} // namespace
PrefsAsh::PrefsAsh(ProfileManager* profile_manager, PrefService* local_state)
: local_state_(local_state) {
DCHECK(profile_manager);
DCHECK(local_state_);
on_app_terminating_subscription_ =
browser_shutdown::AddAppTerminatingCallback(
base::BindOnce(&PrefsAsh::OnAppTerminating, base::Unretained(this)));
profile_manager_observation_.Observe(profile_manager);
local_state_registrar_.Init(local_state_);
}
PrefsAsh::~PrefsAsh() = default;
void PrefsAsh::BindReceiver(mojo::PendingReceiver<mojom::Prefs> receiver) {
receivers_.Add(this, std::move(receiver));
}
void PrefsAsh::GetPref(mojom::PrefPath path, GetPrefCallback callback) {
const base::Value* value = GetValueForState(GetState(path));
std::move(callback).Run(value ? std::optional<base::Value>(value->Clone())
: std::nullopt);
}
void PrefsAsh::GetExtensionPrefWithControl(
mojom::PrefPath path,
GetExtensionPrefWithControlCallback callback) {
auto state = GetState(path);
const base::Value* value = GetValueForState(state);
if (!state || !value) {
// Not a valid prefpath
std::move(callback).Run(std::nullopt,
mojom::PrefControlState::kDefaultUnknown);
return;
}
if (state->pref_source != AshPrefSource::kExtensionControlled) {
// Not extension controlled
std::move(callback).Run(
std::optional<base::Value>(value->Clone()),
mojom::PrefControlState::kNotExtensionControlledPrefPath);
return;
}
mojom::PrefControlState pref_control_state;
// Extension controlled.
const PrefService::Preference* preference =
state->pref_service->FindPreference(state->path);
DCHECK(preference != nullptr);
if (!preference->IsStandaloneBrowserModifiable()) {
pref_control_state = mojom::PrefControlState::kNotExtensionControllable;
} else if (preference->IsStandaloneBrowserControlled()) {
// Lacros has already set this pref. It could be set by any extension
// in lacros.
pref_control_state = mojom::PrefControlState::kLacrosExtensionControlled;
} else {
// Lacros could control this.
pref_control_state = mojom::PrefControlState::kLacrosExtensionControllable;
}
std::move(callback).Run(std::optional<base::Value>(value->Clone()),
pref_control_state);
}
void PrefsAsh::SetPref(mojom::PrefPath path,
base::Value value,
SetPrefCallback callback) {
auto state = GetState(path);
if (state && state->pref_source != AshPrefSource::kCrosSettings) {
if (state->pref_source == AshPrefSource::kExtensionControlled) {
state->pref_service->SetStandaloneBrowserPref(state->path, value);
} else {
state->pref_service->Set(state->path, value);
}
} else {
LOG(WARNING) << "CrosSettings can't be changed via PrefsAsh";
}
std::move(callback).Run();
}
void PrefsAsh::ClearExtensionControlledPref(
mojom::PrefPath path,
ClearExtensionControlledPrefCallback callback) {
auto state = GetState(path);
if (state && state->pref_source == AshPrefSource::kExtensionControlled) {
state->pref_service->RemoveStandaloneBrowserPref(state->path);
} else {
// Only logging to be robust against version skew (lacros ahead of ash)
LOG(WARNING) << "Tried to clear a pref that is not extension controlled";
}
std::move(callback).Run();
}
void PrefsAsh::AddObserver(mojom::PrefPath path,
mojo::PendingRemote<mojom::PrefObserver> observer) {
auto state = GetState(path);
const base::Value* value = GetValueForState(state);
if (!value) {
return;
}
// Fire the observer with the initial value.
mojo::Remote<mojom::PrefObserver> remote(std::move(observer));
remote->OnPrefChanged(value->Clone());
bool did_register = false;
if (state->pref_source == AshPrefSource::kCrosSettings) {
if (cros_settings_subs_.find(path) == cros_settings_subs_.end()) {
// Unretained() is safe since CrosSettings is destroyed after all the
// threads are stopped and PrefsAsh is destroyed while stopping all the
// threads.
cros_settings_subs_.emplace(
path,
ash::CrosSettings::Get()->AddSettingsObserver(
state->path, base::BindRepeating(&PrefsAsh::OnPrefChanged,
base::Unretained(this), path)));
did_register = true;
}
} else {
DCHECK(state->registrar);
if (!state->registrar->IsObserved(state->path)) {
// Unretained() is safe since PrefChangeRegistrar and RemoteSet within
// observers_ are owned by this and wont invoke if PrefsAsh is destroyed.
state->registrar->Add(state->path,
base::BindRepeating(&PrefsAsh::OnPrefChanged,
base::Unretained(this), path));
did_register = true;
}
}
if (did_register) {
observers_[path].set_disconnect_handler(base::BindRepeating(
&PrefsAsh::OnDisconnect, base::Unretained(this), path));
}
observers_[path].Add(std::move(remote));
}
void PrefsAsh::OnProfileAdded(Profile* profile) {
if (!ash::ProfileHelper::IsPrimaryProfile(profile)) {
return;
}
OnPrimaryProfileReady(profile);
}
std::optional<PrefsAsh::State> PrefsAsh::GetState(mojom::PrefPath path) {
switch (path) {
case mojom::PrefPath::kUnknown:
case mojom::PrefPath::kProtectedContentDefaultDeprecated:
case mojom::PrefPath::kDnsOverHttpsTemplates:
case mojom::PrefPath::kDnsOverHttpsTemplatesWithIdentifiers:
case mojom::PrefPath::kDnsOverHttpsSalt:
case mojom::PrefPath::kAccessibilityPdfOcrAlwaysActiveDeprecated:
case mojom::PrefPath::kMahiEnabledDeprecated:
LOG(WARNING) << "Unknown pref path: " << path;
return std::nullopt;
case mojom::PrefPath::kMetricsReportingEnabled:
return State{local_state_, &local_state_registrar_,
AshPrefSource::kNormal,
metrics::prefs::kMetricsReportingEnabled};
case mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled:
case mojom::PrefPath::kUserGeolocationAccessLevel:
case mojom::PrefPath::kQuickAnswersEnabled:
case mojom::PrefPath::kQuickAnswersConsentStatus:
case mojom::PrefPath::kQuickAnswersDefinitionEnabled:
case mojom::PrefPath::kQuickAnswersTranslationEnabled:
case mojom::PrefPath::kQuickAnswersUnitConversionEnabled:
case mojom::PrefPath::kQuickAnswersNoticeImpressionCount:
case mojom::PrefPath::kQuickAnswersNoticeImpressionDuration:
case mojom::PrefPath::kPreferredLanguages:
case mojom::PrefPath::kApplicationLocale:
case mojom::PrefPath::kSharedStorage:
case mojom::PrefPath::kMultitaskMenuNudgeClamshellShownCount:
case mojom::PrefPath::kMultitaskMenuNudgeClamshellLastShown:
case mojom::PrefPath::kAccessCodeCastDevices:
case mojom::PrefPath::kAccessCodeCastDeviceAdditionTime:
case mojom::PrefPath::kDefaultSearchProviderDataPrefName:
case mojom::PrefPath::kIsolatedWebAppsEnabled:
case mojom::PrefPath::kAccessibilityReducedAnimationsEnabled:
case mojom::PrefPath::kHmrEnabled:
case mojom::PrefPath::kUserCameraAllowed:
case mojom::PrefPath::kUserMicrophoneAllowed:
case mojom::PrefPath::kHMRConsentStatus:
case mojom::PrefPath::kHMRConsentWindowDismissCount: {
if (!profile_prefs_registrar_) {
LOG(WARNING) << "Primary profile is not yet initialized";
return std::nullopt;
}
std::string pref_name(GetProfilePrefNameForPref(path));
return State{profile_prefs_registrar_->prefs(),
profile_prefs_registrar_.get(), AshPrefSource::kNormal,
pref_name};
}
case mojom::PrefPath::kDeviceSystemWideTracingEnabled:
return State{local_state_, &local_state_registrar_,
AshPrefSource::kNormal,
ash::prefs::kDeviceSystemWideTracingEnabled};
case mojom::PrefPath::kDnsOverHttpsMode:
return State{local_state_, &local_state_registrar_,
AshPrefSource::kNormal, prefs::kDnsOverHttpsMode};
case mojom::PrefPath::kDnsOverHttpsEffectiveTemplatesChromeOS:
return State{local_state_, &local_state_registrar_,
AshPrefSource::kNormal,
prefs::kDnsOverHttpsEffectiveTemplatesChromeOS};
case mojom::PrefPath::kOverscrollHistoryNavigationEnabled:
return State{local_state_, &local_state_registrar_,
AshPrefSource::kNormal,
prefs::kOverscrollHistoryNavigationEnabled};
case mojom::PrefPath::kDockedMagnifierEnabled:
case mojom::PrefPath::kAccessibilityAutoclickEnabled:
case mojom::PrefPath::kAccessibilityCaretHighlightEnabled:
case mojom::PrefPath::kAccessibilityCursorColorEnabled:
case mojom::PrefPath::kAccessibilityCursorHighlightEnabled:
case mojom::PrefPath::kAccessibilityDictationEnabled:
case mojom::PrefPath::kAccessibilityFocusHighlightEnabled:
case mojom::PrefPath::kAccessibilityHighContrastEnabled:
case mojom::PrefPath::kAccessibilityLargeCursorEnabled:
case mojom::PrefPath::kAccessibilityScreenMagnifierEnabled:
case mojom::PrefPath::kAccessibilitySelectToSpeakEnabled:
case mojom::PrefPath::kExtensionAccessibilitySpokenFeedbackEnabled:
case mojom::PrefPath::kAccessibilityStickyKeysEnabled:
case mojom::PrefPath::kAccessibilitySwitchAccessEnabled:
case mojom::PrefPath::kAccessibilityVirtualKeyboardEnabled:
case mojom::PrefPath::kProxy: {
if (!profile_prefs_registrar_) {
LOG(WARNING) << "Primary profile is not yet initialized";
return std::nullopt;
}
std::string pref_name(GetExtensionPrefNameForPref(path));
return State{profile_prefs_registrar_->prefs(),
profile_prefs_registrar_.get(),
AshPrefSource::kExtensionControlled, pref_name};
}
case mojom::PrefPath::kAttestationForContentProtectionEnabled: {
return State{nullptr, nullptr, AshPrefSource::kCrosSettings,
ash::kAttestationForContentProtectionEnabled};
}
case mojom::PrefPath::kAccessToGetAllScreensMediaInSessionAllowedForUrls:
if (!profile_prefs_registrar_) {
LOG(WARNING) << "Primary profile is not yet initialized";
return std::nullopt;
}
return State{
.pref_service = profile_prefs_registrar_->prefs(),
.registrar = profile_prefs_registrar_.get(),
.pref_source = AshPrefSource::kNormal,
.path =
prefs::kManagedAccessToGetAllScreensMediaInSessionAllowedForUrls};
}
}
const base::Value* PrefsAsh::GetValueForState(std::optional<State> state) {
if (!state) {
return nullptr;
}
if (state->pref_source == AshPrefSource::kCrosSettings) {
return ash::CrosSettings::Get()->GetPref(state->path);
}
return &state->pref_service->GetValue(state->path);
}
void PrefsAsh::OnProfileManagerDestroying() {
profile_manager_observation_.Reset();
}
void PrefsAsh::OnProfileWillBeDestroyed(Profile* profile) {
profile_observation_.Reset();
profile_prefs_registrar_.reset();
}
void PrefsAsh::OnPrefChanged(mojom::PrefPath path) {
auto state = GetState(path);
const base::Value* value = GetValueForState(state);
if (value) {
for (auto& observer : observers_[path]) {
observer->OnPrefChanged(value->Clone());
}
}
}
void PrefsAsh::OnDisconnect(mojom::PrefPath path, mojo::RemoteSetElementId id) {
const auto& it = observers_.find(path);
if (it != observers_.end() && it->second.empty()) {
if (auto state = GetState(path)) {
if (state->pref_source == AshPrefSource::kCrosSettings) {
cros_settings_subs_.erase(path);
} else {
state->registrar->Remove(state->path);
}
}
observers_.erase(it);
}
}
void PrefsAsh::OnPrimaryProfileReady(Profile* profile) {
profile_manager_observation_.Reset();
profile_prefs_registrar_ = std::make_unique<PrefChangeRegistrar>();
profile_prefs_registrar_->Init(profile->GetPrefs());
}
void PrefsAsh::OnAppTerminating() {
profile_prefs_registrar_.reset();
}
} // namespace crosapi