blob: 9069b7966a5f9caf42092c61fb0fe7a934093396 [file] [log] [blame]
// Copyright 2012 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/themes/theme_syncable_service.h"
#include <stddef.h>
#include <string>
#include <utility>
#include "base/auto_reset.h"
#include "base/base64.h"
#include "base/containers/fixed_flat_map.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/one_shot_event.h"
#include "base/strings/stringprintf.h"
#include "base/version.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/background/ntp_custom_background_service_constants.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_utils.h"
#include "chrome/common/extensions/sync_helper.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/features.h"
#include "components/sync/model/sync_change_processor.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/proto_value_conversions.h"
#include "components/sync/protocol/theme_specifics.pb.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/sync_preferences/pref_service_syncable_observer.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pending_extension_info.h"
#include "extensions/browser/pending_extension_manager.h"
#include "extensions/common/manifest_url_handlers.h"
using std::string;
namespace {
struct ThemePrefNames {
std::string_view syncing_pref_name;
std::string_view non_syncing_pref_name;
};
constexpr auto kThemePrefsInMigration =
base::MakeFixedFlatMap<ThemePrefInMigration, ThemePrefNames>({
{ThemePrefInMigration::kBrowserColorScheme,
{prefs::kBrowserColorSchemeDoNotUse,
prefs::kNonSyncingBrowserColorSchemeDoNotUse}},
{ThemePrefInMigration::kUserColor,
{prefs::kUserColorDoNotUse, prefs::kNonSyncingUserColorDoNotUse}},
{ThemePrefInMigration::kBrowserColorVariant,
{prefs::kBrowserColorVariantDoNotUse,
prefs::kNonSyncingBrowserColorVariantDoNotUse}},
{ThemePrefInMigration::kGrayscaleThemeEnabled,
{prefs::kGrayscaleThemeEnabledDoNotUse,
prefs::kNonSyncingGrayscaleThemeEnabledDoNotUse}},
{ThemePrefInMigration::kNtpCustomBackgroundDict,
{prefs::kNtpCustomBackgroundDictDoNotUse,
prefs::kNonSyncingNtpCustomBackgroundDictDoNotUse}},
});
static_assert(
kThemePrefsInMigration.size() ==
static_cast<size_t>(ThemePrefInMigration::kMaxValue) + 1,
"ThemePrefInMigration entry missing from kThemePrefsInMigration map.");
bool IsTheme(const extensions::Extension* extension,
content::BrowserContext* context) {
return extension->is_theme();
}
bool HasNonDefaultBrowserColorScheme(
const sync_pb::ThemeSpecifics& theme_specifics) {
return theme_specifics.has_browser_color_scheme() &&
ProtoEnumToBrowserColorScheme(
theme_specifics.browser_color_scheme()) !=
ThemeService::BrowserColorScheme::kSystem;
}
std::optional<base::Value::Dict> NtpBackgroundDictFromSpecifics(
const sync_pb::ThemeSpecifics& theme_specifics) {
if (!theme_specifics.has_ntp_background()) {
return std::nullopt;
}
const sync_pb::ThemeSpecifics::NtpCustomBackground& ntp_background =
theme_specifics.ntp_background();
base::Value::Dict dict;
if (ntp_background.has_url()) {
dict.Set(kNtpCustomBackgroundURL, ntp_background.url());
}
if (ntp_background.has_attribution_line_1()) {
dict.Set(kNtpCustomBackgroundAttributionLine1,
ntp_background.attribution_line_1());
}
if (ntp_background.has_attribution_line_2()) {
dict.Set(kNtpCustomBackgroundAttributionLine2,
ntp_background.attribution_line_2());
}
if (ntp_background.has_attribution_action_url()) {
dict.Set(kNtpCustomBackgroundAttributionActionURL,
ntp_background.attribution_action_url());
}
if (ntp_background.has_collection_id()) {
dict.Set(kNtpCustomBackgroundCollectionId, ntp_background.collection_id());
}
if (ntp_background.has_resume_token()) {
dict.Set(kNtpCustomBackgroundResumeToken, ntp_background.resume_token());
}
if (ntp_background.has_refresh_timestamp_unix_epoch_seconds()) {
dict.Set(kNtpCustomBackgroundRefreshTimestamp,
static_cast<int>(
ntp_background.refresh_timestamp_unix_epoch_seconds()));
}
if (ntp_background.has_main_color()) {
dict.Set(kNtpCustomBackgroundMainColor,
static_cast<int>(ntp_background.main_color()));
}
return dict;
}
sync_pb::ThemeSpecifics::NtpCustomBackground SpecificsNtpBackgroundFromDict(
const base::Value::Dict& dict) {
sync_pb::ThemeSpecifics::NtpCustomBackground ntp_background;
if (const std::string* value = dict.FindString(kNtpCustomBackgroundURL)) {
ntp_background.set_url(*value);
}
if (const std::string* value =
dict.FindString(kNtpCustomBackgroundAttributionLine1)) {
ntp_background.set_attribution_line_1(*value);
}
if (const std::string* value =
dict.FindString(kNtpCustomBackgroundAttributionLine2)) {
ntp_background.set_attribution_line_2(*value);
}
if (const std::string* value =
dict.FindString(kNtpCustomBackgroundAttributionActionURL)) {
ntp_background.set_attribution_action_url(*value);
}
if (const std::string* value =
dict.FindString(kNtpCustomBackgroundCollectionId)) {
ntp_background.set_collection_id(*value);
}
if (const std::string* value =
dict.FindString(kNtpCustomBackgroundResumeToken)) {
ntp_background.set_resume_token(*value);
}
if (std::optional<int> value =
dict.FindInt(kNtpCustomBackgroundRefreshTimestamp)) {
ntp_background.set_refresh_timestamp_unix_epoch_seconds(*value);
}
if (std::optional<int> value = dict.FindInt(kNtpCustomBackgroundMainColor)) {
ntp_background.set_main_color(*value);
}
return ntp_background;
}
bool AreSpecificsNtpBackgroundEquivalent(
const sync_pb::ThemeSpecifics::NtpCustomBackground& a,
const sync_pb::ThemeSpecifics::NtpCustomBackground& b) {
// MessageDifferencer cannot be used and explicitly comparing all the fields
// is maintenance-heavy.
return a.SerializeAsString() == b.SerializeAsString();
}
} // namespace
// "Current" is part of the name for historical reasons, shouldn't be changed.
const char ThemeSyncableService::kSyncEntityClientTag[] = "current_theme";
const char ThemeSyncableService::kSyncEntityTitle[] = "Current Theme";
std::string_view GetThemePrefNameInMigration(ThemePrefInMigration theme_pref) {
const ThemePrefNames& theme_pref_names =
kThemePrefsInMigration.at(theme_pref);
return base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics)
? theme_pref_names.non_syncing_pref_name
: theme_pref_names.syncing_pref_name;
}
void MigrateSyncingThemePrefsToNonSyncingIfNeeded(PrefService* prefs) {
if (!base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics)) {
// Clear migration flag to allow re-migration when the feature flag is
// re-enabled.
prefs->ClearPref(prefs::kSyncingThemePrefsMigratedToNonSyncing);
return;
}
const bool already_migrated =
prefs->GetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing);
base::UmaHistogramBoolean("Theme.ThemePrefMigration.AlreadyMigrated",
already_migrated);
if (already_migrated) {
return;
}
for (const auto& [pref_in_migration, pref_names] : kThemePrefsInMigration) {
if (const base::Value* value =
prefs->GetUserPrefValue(pref_names.syncing_pref_name)) {
prefs->Set(pref_names.non_syncing_pref_name, value->Clone());
base::UmaHistogramEnumeration("Theme.ThemePrefMigration.MigratedPref",
pref_in_migration);
}
}
prefs->SetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing, true);
}
class ThemeSyncableService::PrefServiceSyncableObserver
: public sync_preferences::PrefServiceSyncableObserver {
public:
PrefServiceSyncableObserver(sync_preferences::PrefServiceSyncable* prefs,
ThemeSyncableService* theme_syncable_service)
: prefs_(prefs), theme_syncable_service_(theme_syncable_service) {
observation_.Observe(prefs);
// Prefs sync might have already started.
OnIsSyncingChanged();
}
void OnIsSyncingChanged() override {
CHECK(prefs_->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
if (prefs_->IsSyncing()) {
observation_.Reset();
bool should_notify = false;
{
// Block self-induced notifications (see crbug.com/375553464).
base::AutoReset<bool> processing_changes(
&theme_syncable_service_->processing_syncer_changes_, true);
// Copy over synced pref values to the new theme prefs.
for (const auto& [pref_in_migration, pref_names] :
kThemePrefsInMigration) {
if (const base::Value* value =
prefs_->GetUserPrefValue(pref_names.syncing_pref_name)) {
// User color pref needs another pref to be set to be detected.
if (pref_in_migration == ThemePrefInMigration::kUserColor) {
prefs_->SetString(prefs::kCurrentThemeID,
ThemeService::kUserColorThemeID);
}
prefs_->Set(pref_names.non_syncing_pref_name, value->Clone());
base::UmaHistogramEnumeration(
"Theme.ThemePrefMigration.IncomingSyncingPrefApplied",
pref_in_migration);
should_notify = true;
}
}
}
prefs_->SetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs, false);
if (should_notify) {
theme_syncable_service_->OnThemeChanged();
}
}
}
private:
base::ScopedObservation<sync_preferences::PrefServiceSyncable,
sync_preferences::PrefServiceSyncableObserver>
observation_{this};
raw_ptr<sync_preferences::PrefServiceSyncable> prefs_;
raw_ptr<ThemeSyncableService> theme_syncable_service_;
};
ThemeSyncableService::ThemeSyncableService(Profile* profile,
ThemeService* theme_service)
: profile_(profile),
theme_service_(theme_service),
use_system_theme_by_default_(false) {
CHECK(profile_);
CHECK(profile_->GetPrefs());
DCHECK(theme_service_);
theme_service_->AddObserver(this);
sync_preferences::PrefServiceSyncable* prefs =
static_cast<sync_preferences::PrefServiceSyncable*>(profile_->GetPrefs());
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics)) {
// Listen to NtpCustomBackgroundDict pref changes. This is done because
// ThemeService doesn't convey ntp background change notifications.
pref_change_registrar_.Init(prefs);
pref_change_registrar_.Add(
prefs::kNonSyncingNtpCustomBackgroundDictDoNotUse,
base::BindRepeating(&ThemeSyncableService::OnThemeChanged,
base::Unretained(this)));
if (prefs->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs)) {
// ThemeSyncableService instance is destroyed upon ThemeService::Shutdown.
// So `prefs` outlives this.
pref_service_syncable_observer_ =
std::make_unique<PrefServiceSyncableObserver>(
prefs,
// This is okay since `this` outlives
// `pref_service_syncable_observer_`.
this);
}
} else {
// Reset flag to allow reading the syncing prefs once again when
// kMoveThemePrefsToSpecifics feature is re-enabled.
prefs->SetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs, true);
}
}
ThemeSyncableService::~ThemeSyncableService() {
pref_service_syncable_observer_.reset();
theme_service_->RemoveObserver(this);
}
void ThemeSyncableService::OnThemeChanged() {
if (sync_processor_.get() && !processing_syncer_changes_ &&
IsCurrentThemeSyncable()) {
const sync_pb::ThemeSpecifics current_specifics =
GetThemeSpecificsFromCurrentTheme();
ProcessNewTheme(syncer::SyncChange::ACTION_UPDATE, current_specifics);
use_system_theme_by_default_ =
current_specifics.use_system_theme_by_default();
}
}
void ThemeSyncableService::AddObserver(
ThemeSyncableService::Observer* observer) {
observer_list_.AddObserver(observer);
}
void ThemeSyncableService::RemoveObserver(
ThemeSyncableService::Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void ThemeSyncableService::NotifyOnSyncStartedForTesting(
ThemeSyncState startup_state) {
NotifyOnSyncStarted(startup_state);
}
std::optional<ThemeSyncableService::ThemeSyncState>
ThemeSyncableService::GetThemeSyncStartState() {
return startup_state_;
}
void ThemeSyncableService::WaitUntilReadyToSync(base::OnceClosure done) {
extensions::ExtensionSystem::Get(profile_)->ready().Post(FROM_HERE,
std::move(done));
}
void ThemeSyncableService::WillStartInitialSync() {
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics) &&
base::FeatureList::IsEnabled(syncer::kSeparateLocalAndAccountThemes)) {
// Save current theme specifics to pref. This is used to restore the local
// theme upon signout.
profile_->GetPrefs()->SetString(
prefs::kSavedLocalTheme,
base::Base64Encode(
GetThemeSpecificsFromCurrentTheme().SerializeAsString()));
}
}
std::optional<syncer::ModelError>
ThemeSyncableService::MergeDataAndStartSyncing(
syncer::DataType type,
const syncer::SyncDataList& initial_sync_data,
std::unique_ptr<syncer::SyncChangeProcessor> sync_processor) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!sync_processor_.get());
DCHECK(sync_processor.get());
sync_processor_ = std::move(sync_processor);
if (initial_sync_data.size() > 1) {
return syncer::ModelError(
FROM_HERE,
base::StringPrintf("Received %d theme specifics.",
static_cast<int>(initial_sync_data.size())));
}
if (!IsCurrentThemeSyncable()) {
// Current theme is unsyncable - don't overwrite from sync data, and don't
// save the unsyncable theme to sync data.
NotifyOnSyncStarted(ThemeSyncState::kFailed);
return std::nullopt;
}
const sync_pb::ThemeSpecifics current_specifics =
GetThemeSpecificsFromCurrentTheme();
if (!initial_sync_data.empty() &&
initial_sync_data[0].GetSpecifics().has_theme()) {
const sync_pb::ThemeSpecifics& new_specifics =
initial_sync_data[0].GetSpecifics().theme();
if (!HasNonDefaultTheme(current_specifics) ||
HasNonDefaultTheme(new_specifics)) {
ThemeSyncState startup_state =
MaybeSetTheme(current_specifics, new_specifics);
// Commit the current theme if it has changed and is different from the
// remote theme. This can happen when theme attributes which were
// earlier synced via prefs (user color and ntp background), are now
// populated in ThemeSpecifics. This new ThemeSpecifics should be
// committed to the server. Note that this is avoided for incoming
// extension themes as they are applied from a posted task and will call
// OnThemeChanged() when set and commit the current theme.
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics) &&
startup_state == ThemeSyncState::kApplied &&
!new_specifics.use_custom_theme() &&
!AreThemeSpecificsEquivalent(
GetThemeSpecificsFromCurrentTheme(), new_specifics,
theme_service_->IsSystemThemeDistinctFromDefaultTheme())) {
OnThemeChanged();
}
NotifyOnSyncStarted(startup_state);
return std::nullopt;
}
}
// No theme specifics found. Commit one according to current theme if
// kSeparateLocalAndAccountThemes feature flag is not enabled.
std::optional<syncer::ModelError> error =
base::FeatureList::IsEnabled(syncer::kSeparateLocalAndAccountThemes)
? std::nullopt
: ProcessNewTheme(syncer::SyncChange::ACTION_ADD, current_specifics);
NotifyOnSyncStarted(ThemeSyncState::kApplied);
return error;
}
void ThemeSyncableService::StopSyncing(syncer::DataType type) {
CHECK(thread_checker_.CalledOnValidThread());
CHECK_EQ(type, syncer::THEMES);
sync_processor_.reset();
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics) &&
base::FeatureList::IsEnabled(syncer::kSeparateLocalAndAccountThemes)) {
// It is possible that saved local theme was cleared by the batch uploader.
// In such a case, apply the default theme.
const bool result = ApplySavedLocalThemeIfExistsAndClear();
base::UmaHistogramBoolean("Theme.RestoredLocalThemeUponSignout", result);
if (!result) {
theme_service_->UseDefaultTheme();
}
}
}
void ThemeSyncableService::OnBrowserShutdown(syncer::DataType type) {
CHECK(thread_checker_.CalledOnValidThread());
CHECK_EQ(type, syncer::THEMES);
sync_processor_.reset();
}
syncer::SyncDataList ThemeSyncableService::GetAllSyncDataForTesting(
syncer::DataType type) const {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(type, syncer::THEMES);
syncer::SyncDataList list;
if (IsCurrentThemeSyncable()) {
sync_pb::EntitySpecifics entity_specifics;
*entity_specifics.mutable_theme() = GetThemeSpecificsFromCurrentTheme();
list.push_back(syncer::SyncData::CreateLocalData(
kSyncEntityClientTag, kSyncEntityTitle, entity_specifics));
}
return list;
}
std::optional<syncer::ModelError> ThemeSyncableService::ProcessSyncChanges(
const base::Location& from_here,
const syncer::SyncChangeList& change_list) {
DCHECK(thread_checker_.CalledOnValidThread());
if (!sync_processor_.get()) {
return syncer::ModelError(FROM_HERE,
"Theme syncable service is not started.");
}
// TODO(akalin): Normally, we should only have a single change and
// it should be an update. However, the syncapi may occasionally
// generates multiple changes. When we fix syncapi to not do that,
// we can remove the extra logic below. See:
// http://code.google.com/p/chromium/issues/detail?id=41696 .
if (change_list.size() != 1) {
string err_msg = base::StringPrintf("Received %d theme changes: ",
static_cast<int>(change_list.size()));
for (const auto& i : change_list) {
base::StringAppendF(&err_msg, "[%s] ", i.ToString().c_str());
}
return syncer::ModelError(FROM_HERE, err_msg);
}
const syncer::SyncChange& theme_change = change_list[0];
if (theme_change.change_type() != syncer::SyncChange::ACTION_ADD &&
theme_change.change_type() != syncer::SyncChange::ACTION_UPDATE) {
return syncer::ModelError(
FROM_HERE, "Invalid theme change: " + theme_change.ToString());
}
if (!IsCurrentThemeSyncable()) {
// Current theme is unsyncable, so don't overwrite it.
return std::nullopt;
}
// Set current theme from the theme specifics.
if (theme_change.sync_data().GetSpecifics().has_theme()) {
MaybeSetTheme(GetThemeSpecificsFromCurrentTheme(),
theme_change.sync_data().GetSpecifics().theme());
return std::nullopt;
}
return syncer::ModelError(FROM_HERE, "Didn't find valid theme specifics");
}
base::WeakPtr<syncer::SyncableService> ThemeSyncableService::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
ThemeSyncableService::ThemeSyncState ThemeSyncableService::MaybeSetTheme(
const sync_pb::ThemeSpecifics& current_specs,
const sync_pb::ThemeSpecifics& new_specs) {
use_system_theme_by_default_ = new_specs.use_system_theme_by_default();
if (AreThemeSpecificsEquivalent(
current_specs, new_specs,
theme_service_->IsSystemThemeDistinctFromDefaultTheme())) {
DVLOG(1) << "Skip setting theme because specs are equal";
return ThemeSyncState::kApplied;
}
const bool use_new_fields =
base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics);
// Whether the ThemeSpecifics is from a client which commits all theme
// attributes via ThemeSpecifics.
const bool has_all_theme_attributes = new_specs.has_browser_color_scheme();
// The new specifics will always include `browser_color_scheme` field. If it
// is absent and the theme specifics is the default theme, avoid setting to
// default theme. This is because the old clients can send such specifics upon
// any change to theme sent via preferences which the new clients do not read.
if (use_new_fields && !has_all_theme_attributes &&
!HasNonDefaultTheme(new_specs)) {
DVLOG(1) << "Skip setting default theme from old clients";
return ThemeSyncState::kApplied;
}
base::AutoReset<bool> processing_changes(&processing_syncer_changes_, true);
// Browser color scheme can be set alongside other themes, including extension
// theme.
if (use_new_fields && has_all_theme_attributes) {
DVLOG(1) << "Applying browser color scheme";
theme_service_->SetBrowserColorScheme(
ProtoEnumToBrowserColorScheme(new_specs.browser_color_scheme()));
// Prior to the ThemeSpecifics migration (crbug.com/356148174),
// 'browser_color_scheme' was absent. Post-migration, it's always set. If
// this field exists, a newer theme has been synced, making reading the
// syncing theme prefs pointless.
profile_->GetPrefs()->SetBoolean(
prefs::kShouldReadIncomingSyncingThemePrefs, false);
pref_service_syncable_observer_.reset();
}
if (new_specs.use_custom_theme()) {
string id(new_specs.custom_theme_id());
GURL update_url(new_specs.custom_theme_update_url());
DVLOG(1) << "Applying theme " << id << " with update_url " << update_url;
extensions::ExtensionService* extension_service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
CHECK(extension_service);
extensions::ExtensionRegistry* extension_registry =
extensions::ExtensionRegistry::Get(profile_);
CHECK(extension_registry);
const extensions::Extension* extension =
extension_registry->GetExtensionById(
id, extensions::ExtensionRegistry::EVERYTHING);
if (extension) {
if (!extension->is_theme()) {
DVLOG(1) << "Extension " << id << " is not a theme; aborting";
return ThemeSyncState::kFailed;
}
auto* extension_registrar = extensions::ExtensionRegistrar::Get(profile_);
if (extension_registrar->IsExtensionEnabled(id)) {
// An enabled theme extension with the given id was found, so
// just set the current theme to it.
theme_service_->SetTheme(extension);
return ThemeSyncState::kApplied;
}
bool is_disabled_by_user =
extensions::ExtensionPrefs::Get(profile_)->HasOnlyDisableReason(
id, extensions::disable_reason::DISABLE_USER_ACTION);
if (is_disabled_by_user) {
// The user had installed this theme but disabled it (by installing
// another atop it); re-enable.
theme_service_->RevertToExtensionTheme(id);
return ThemeSyncState::kApplied;
}
DVLOG(1) << "Theme " << id
<< " is disabled with reasons other than DISABLE_USER_ACTION "
<< "; aborting";
return ThemeSyncState::kFailed;
}
// No extension with this id exists -- we must install it; we do
// so by adding it as a pending extension and then triggering an
// auto-update cycle.
const bool kRemoteInstall = false;
if (!extensions::PendingExtensionManager::Get(profile_)->AddFromSync(
id, update_url, base::Version(), &IsTheme, kRemoteInstall)) {
LOG(WARNING) << "Could not add pending extension for " << id;
return ThemeSyncState::kFailed;
}
remote_extension_theme_pending_install_ = id;
extension_service->CheckForUpdatesSoon();
// Return that the call triggered an extension theme installation.
return ThemeSyncState::kWaitingForExtensionInstallation;
}
// Apply theme besides the NTP background and the browser color scheme. These
// themes cannot exist alongside each other.
if (use_new_fields && new_specs.has_user_color_theme() &&
new_specs.user_color_theme().has_color() &&
new_specs.user_color_theme().has_browser_color_variant()) {
DVLOG(1) << "Applying user color";
theme_service_->SetUserColorAndBrowserColorVariant(
new_specs.user_color_theme().color(),
ProtoEnumToBrowserColorVariant(
new_specs.user_color_theme().browser_color_variant()));
} else if (use_new_fields && new_specs.has_grayscale_theme_enabled()) {
DVLOG(1) << "Applying grayscale theme";
theme_service_->SetIsGrayscale(/*is_grayscale=*/true);
} else if (new_specs.has_autogenerated_color_theme()) {
DVLOG(1) << "Applying autogenerated theme";
theme_service_->BuildAutogeneratedThemeFromColor(
new_specs.autogenerated_color_theme().color());
} else if (new_specs.use_system_theme_by_default()) {
DVLOG(1) << "Switch to use system theme";
theme_service_->UseSystemTheme();
} else {
// NOTE: No need to check for `is_new_specifics` before setting to default
// theme. Empty incoming themes are ignored in MergeDataAndStartSyncing().
DVLOG(1) << "Switch to use default theme";
theme_service_->UseDefaultTheme();
}
if (use_new_fields) {
PrefService* prefs = profile_->GetPrefs();
// NTP background can exist along with the other (non-extension) themes.
if (std::optional<base::Value::Dict> dict =
NtpBackgroundDictFromSpecifics(new_specs);
dict && !dict->empty()) {
DVLOG(1) << "Applying custom NTP background";
// TODO(crbug.com/356148174): Set via NtpCustomBackgroundService instead
// of setting the pref directly.
prefs->SetDict(prefs::kNonSyncingNtpCustomBackgroundDictDoNotUse,
std::move(*dict));
} else if (has_all_theme_attributes) {
// Clear the current ntp background if none received from remote.
// NOTE: Ntp background is only cleared if the incoming ThemeSpecifics
// is the new one and is missing the ntp_background field because it was
// committed by an old client.
DVLOG(1) << "Removing custom NTP background";
prefs->ClearPref(prefs::kNonSyncingNtpCustomBackgroundDictDoNotUse);
}
}
return ThemeSyncState::kApplied;
}
bool ThemeSyncableService::IsCurrentThemeSyncable() const {
const std::string theme_id = theme_service_->GetThemeID();
const extensions::Extension* current_extension =
theme_service_->UsingExtensionTheme() &&
!theme_service_->UsingDefaultTheme()
? extensions::ExtensionRegistry::Get(profile_)
->enabled_extensions()
.GetByID(theme_id)
: nullptr;
if (current_extension &&
!extensions::sync_helper::IsSyncable(current_extension)) {
DVLOG(1) << "Ignoring non-syncable extension: " << current_extension->id();
return false;
}
// If theme was set through policy, it should be unsyncable.
if (theme_service_->UsingPolicyTheme()) {
return false;
}
return true;
}
sync_pb::ThemeSpecifics
ThemeSyncableService::GetThemeSpecificsFromCurrentTheme() const {
sync_pb::ThemeSpecifics theme_specifics;
theme_specifics.set_use_custom_theme(false);
// Set this to `use_system_theme_by_default_` which is the value received from
// sync. If this platform supports distinct system theme, the value might be
// overridden below depending on the current theme.
theme_specifics.set_use_system_theme_by_default(use_system_theme_by_default_);
const bool set_all_theme_attributes =
base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics);
// Always set the browser color scheme, to denote that the ThemeSpecifics
// contains all the theme attributes.
if (set_all_theme_attributes) {
theme_specifics.set_browser_color_scheme(
BrowserColorSchemeToProtoEnum(theme_service_->GetBrowserColorScheme()));
}
const std::string theme_id = theme_service_->GetThemeID();
const extensions::Extension* current_extension =
theme_service_->UsingExtensionTheme() &&
!theme_service_->UsingDefaultTheme()
? extensions::ExtensionRegistry::Get(profile_)
->enabled_extensions()
.GetByID(theme_id)
: nullptr;
if (current_extension) {
// Using custom theme and it's an extension.
DCHECK(current_extension->is_theme());
theme_specifics.set_use_custom_theme(true);
theme_specifics.set_custom_theme_name(current_extension->name());
theme_specifics.set_custom_theme_id(current_extension->id());
theme_specifics.set_custom_theme_update_url(
extensions::ManifestURL::GetUpdateURL(current_extension).spec());
return theme_specifics;
}
if (set_all_theme_attributes) {
// Skip setting background in the specifics if the background is set using
// local resource.
PrefService* prefs = profile_->GetPrefs();
if (!prefs->GetBoolean(prefs::kNtpCustomBackgroundLocalToDevice)) {
// Fetch ntp background dict from pref.
// TODO(crbug.com/356148174): Query NtpCustomBackgroundService instead.
if (const base::Value* pref = prefs->GetUserPrefValue(
prefs::kNonSyncingNtpCustomBackgroundDictDoNotUse)) {
*theme_specifics.mutable_ntp_background() =
SpecificsNtpBackgroundFromDict(pref->GetDict());
}
}
theme_specifics.set_browser_color_scheme(
BrowserColorSchemeToProtoEnum(theme_service_->GetBrowserColorScheme()));
if (theme_service_->GetIsGrayscale()) {
theme_specifics.mutable_grayscale_theme_enabled();
} else if (ThemeService::kUserColorThemeID == theme_id) {
if (const std::optional<SkColor> user_color =
theme_service_->GetUserColor()) {
sync_pb::ThemeSpecifics::UserColorTheme* user_color_theme =
theme_specifics.mutable_user_color_theme();
user_color_theme->set_color(*user_color);
user_color_theme->set_browser_color_variant(
BrowserColorVariantToProtoEnum(
theme_service_->GetBrowserColorVariant()));
}
}
}
if (theme_service_->UsingAutogeneratedTheme()) {
// Using custom theme and it's autogenerated from color.
theme_specifics.set_use_custom_theme(false);
theme_specifics.mutable_autogenerated_color_theme()->set_color(
theme_service_->GetAutogeneratedThemeColor());
}
if (theme_service_->IsSystemThemeDistinctFromDefaultTheme()) {
// On platform where system theme is different from default theme, set
// use_system_theme_by_default to true if system theme is used, false
// if default system theme is used. Otherwise keep it to the value received
// from sync (`use_system_theme_by_default_`).
if (theme_service_->UsingSystemTheme()) {
theme_specifics.set_use_system_theme_by_default(true);
} else if (theme_service_->UsingDefaultTheme()) {
theme_specifics.set_use_system_theme_by_default(false);
}
}
return theme_specifics;
}
sync_pb::ThemeSpecifics
ThemeSyncableService::GetThemeSpecificsFromCurrentThemeForTesting() const {
return GetThemeSpecificsFromCurrentTheme();
}
/* static */
bool ThemeSyncableService::AreThemeSpecificsEquivalent(
const sync_pb::ThemeSpecifics& a,
const sync_pb::ThemeSpecifics& b,
bool is_system_theme_distinct_from_default_theme) {
if (HasNonDefaultTheme(a) != HasNonDefaultTheme(b)) {
return false;
}
if (a.use_custom_theme() || b.use_custom_theme()) {
// We're using an extensions theme, so simply compare IDs since those
// are guaranteed unique.
return a.use_custom_theme() == b.use_custom_theme() &&
a.custom_theme_id() == b.custom_theme_id();
}
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics)) {
// Since browser color scheme and ntp background can coexist with all other
// theme types, they're the first ones tested.
// Compare the two ntp background dicts as whole.
if ((a.has_ntp_background() || b.has_ntp_background()) &&
!AreSpecificsNtpBackgroundEquivalent(a.ntp_background(),
b.ntp_background())) {
return false;
}
if (ProtoEnumToBrowserColorScheme(a.browser_color_scheme()) !=
ProtoEnumToBrowserColorScheme(b.browser_color_scheme())) {
return false;
}
if (a.has_user_color_theme() || b.has_user_color_theme()) {
return a.has_user_color_theme() == b.has_user_color_theme() &&
a.user_color_theme().color() == b.user_color_theme().color() &&
ProtoEnumToBrowserColorVariant(
a.user_color_theme().browser_color_variant()) ==
ProtoEnumToBrowserColorVariant(
b.user_color_theme().browser_color_variant());
}
if (a.has_grayscale_theme_enabled() || b.has_grayscale_theme_enabled()) {
return a.has_grayscale_theme_enabled() == b.has_grayscale_theme_enabled();
}
}
if (a.has_autogenerated_color_theme() || b.has_autogenerated_color_theme()) {
return a.has_autogenerated_color_theme() ==
b.has_autogenerated_color_theme() &&
a.autogenerated_color_theme().color() ==
b.autogenerated_color_theme().color();
}
if (is_system_theme_distinct_from_default_theme) {
// We're not using a custom theme, but we care about system
// vs. default.
return a.use_system_theme_by_default() == b.use_system_theme_by_default();
}
// We're not using a custom theme, and we don't care about system
// vs. default.
return true;
}
bool ThemeSyncableService::HasNonDefaultTheme(
const sync_pb::ThemeSpecifics& theme_specifics) {
return theme_specifics.use_custom_theme() ||
theme_specifics.has_autogenerated_color_theme() ||
(base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics) &&
(theme_specifics.has_user_color_theme() ||
theme_specifics.has_grayscale_theme_enabled() ||
HasNonDefaultBrowserColorScheme(theme_specifics) ||
theme_specifics.has_ntp_background()));
}
std::optional<syncer::ModelError> ThemeSyncableService::ProcessNewTheme(
syncer::SyncChange::SyncChangeType change_type,
const sync_pb::ThemeSpecifics& theme_specifics) {
// As part of the theme migration strategy, update the old syncing prefs with
// the new values.
PrefService* prefs = profile_->GetPrefs();
if (base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics)) {
for (const auto& [pref_in_migration, pref_names] : kThemePrefsInMigration) {
// Skip setting ntp background pref if the background is currently set
// using a local resource.
if (pref_in_migration == ThemePrefInMigration::kNtpCustomBackgroundDict &&
prefs->GetBoolean(prefs::kNtpCustomBackgroundLocalToDevice)) {
continue;
}
if (const base::Value* value =
prefs->GetUserPrefValue(pref_names.non_syncing_pref_name)) {
prefs->Set(pref_names.syncing_pref_name, *value);
} else {
prefs->ClearPref(pref_names.syncing_pref_name);
}
}
}
syncer::SyncChangeList changes;
sync_pb::EntitySpecifics entity_specifics;
entity_specifics.mutable_theme()->CopyFrom(theme_specifics);
changes.emplace_back(
FROM_HERE, change_type,
syncer::SyncData::CreateLocalData(kSyncEntityClientTag, kSyncEntityTitle,
entity_specifics));
DVLOG(1) << "Update theme specifics from current theme: "
<< changes.back().ToString();
return sync_processor_->ProcessSyncChanges(FROM_HERE, changes);
}
void ThemeSyncableService::NotifyOnSyncStarted(ThemeSyncState startup_state) {
// Keep the state for later calls to GetThemeSyncStartState().
startup_state_ = startup_state;
for (Observer& observer : observer_list_) {
observer.OnThemeSyncStarted(startup_state);
}
}
std::optional<sync_pb::ThemeSpecifics>
ThemeSyncableService::GetSavedLocalTheme() const {
CHECK(base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics));
CHECK(base::FeatureList::IsEnabled(syncer::kSeparateLocalAndAccountThemes));
if (const base::Value* saved_local_theme =
profile_->GetPrefs()->GetUserPrefValue(prefs::kSavedLocalTheme)) {
std::string decoded_str;
sync_pb::ThemeSpecifics specifics;
// The local theme is saved as a base64 encoded string.
if (base::Base64Decode(saved_local_theme->GetString(), &decoded_str) &&
specifics.ParseFromString(decoded_str)) {
return specifics;
}
}
return std::nullopt;
}
bool ThemeSyncableService::ApplySavedLocalThemeIfExistsAndClear() {
CHECK(base::FeatureList::IsEnabled(syncer::kMoveThemePrefsToSpecifics));
CHECK(base::FeatureList::IsEnabled(syncer::kSeparateLocalAndAccountThemes));
std::optional<sync_pb::ThemeSpecifics> local_theme_specifics =
GetSavedLocalTheme();
if (local_theme_specifics) {
// This does not trigger a notification to OnThemeChanged() and thus does
// not commit the theme change to sync. That is done below.
MaybeSetTheme(GetThemeSpecificsFromCurrentTheme(), *local_theme_specifics);
if (remote_extension_theme_pending_install_) {
extensions::PendingExtensionManager* pending_extension_manager =
extensions::PendingExtensionManager::Get(profile_);
// If the theme extension is still pending installation, remove from the
// queue.
if (const extensions::PendingExtensionInfo* extension =
pending_extension_manager->GetById(
*remote_extension_theme_pending_install_);
extension && extension->is_from_sync()) {
pending_extension_manager->Remove(
*remote_extension_theme_pending_install_);
}
// Remove any unused theme extension. This should remove
// `remote_extension_theme_pending_install_` if it was installed.
theme_service_->RemoveUnusedThemes();
}
// Commit the theme change to sync. Note that this does not trigger a commit
// when called while StopSyncing().
OnThemeChanged();
}
profile_->GetPrefs()->ClearPref(prefs::kSavedLocalTheme);
return local_theme_specifics.has_value();
}