blob: 521fc58a01f6996d5fcf89046a99ed50dc30b2bd [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/containers/adapters.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/themes/theme_service.h"
#include "chrome/common/extensions/sync_helper.h"
#include "components/sync/model/sync_change_processor.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/theme_specifics.pb.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/manifest_url_handlers.h"
using std::string;
namespace {
bool IsTheme(const extensions::Extension* extension,
content::BrowserContext* context) {
return extension->is_theme();
}
} // 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";
ThemeSyncableService::ThemeSyncableService(Profile* profile,
ThemeService* theme_service)
: profile_(profile),
theme_service_(theme_service),
use_system_theme_by_default_(false) {
DCHECK(theme_service_);
theme_service_->AddObserver(this);
}
ThemeSyncableService::~ThemeSyncableService() {
theme_service_->RemoveObserver(this);
}
void ThemeSyncableService::OnThemeChanged() {
if (sync_processor_.get()) {
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(&current_specifics))
return; // Current theme is unsyncable.
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));
}
std::optional<syncer::ModelError>
ThemeSyncableService::MergeDataAndStartSyncing(
syncer::ModelType 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())));
}
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(&current_specifics)) {
// 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;
}
// Find the last SyncData that has theme data and set the current theme from
// it. If SyncData doesn't have a theme, but there is a current theme, it will
// not reset it.
for (const syncer::SyncData& sync_data : base::Reversed(initial_sync_data)) {
if (sync_data.GetSpecifics().has_theme()) {
if (!HasNonDefaultTheme(current_specifics) ||
HasNonDefaultTheme(sync_data.GetSpecifics().theme())) {
ThemeSyncState startup_state =
MaybeSetTheme(current_specifics, sync_data);
NotifyOnSyncStarted(startup_state);
return std::nullopt;
}
}
}
// No theme specifics are found. Create one according to current theme.
std::optional<syncer::ModelError> error =
ProcessNewTheme(syncer::SyncChange::ACTION_ADD, current_specifics);
NotifyOnSyncStarted(ThemeSyncState::kApplied);
return error;
}
void ThemeSyncableService::StopSyncing(syncer::ModelType type) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(type, syncer::THEMES);
sync_processor_.reset();
}
syncer::SyncDataList ThemeSyncableService::GetAllSyncDataForTesting(
syncer::ModelType type) const {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(type, syncer::THEMES);
syncer::SyncDataList list;
sync_pb::EntitySpecifics entity_specifics;
if (GetThemeSpecificsFromCurrentTheme(entity_specifics.mutable_theme())) {
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 (size_t i = 0; i < change_list.size(); ++i) {
base::StringAppendF(&err_msg, "[%s] ", change_list[i].ToString().c_str());
}
return syncer::ModelError(FROM_HERE, err_msg);
}
if (change_list.begin()->change_type() != syncer::SyncChange::ACTION_ADD &&
change_list.begin()->change_type() != syncer::SyncChange::ACTION_UPDATE) {
return syncer::ModelError(
FROM_HERE, "Invalid theme change: " + change_list.begin()->ToString());
}
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(&current_specifics)) {
// Current theme is unsyncable, so don't overwrite it.
return std::nullopt;
}
// Set current theme from the theme specifics of the last change of type
// |ACTION_ADD| or |ACTION_UPDATE|.
for (const syncer::SyncChange& theme_change : base::Reversed(change_list)) {
if (theme_change.sync_data().GetSpecifics().has_theme() &&
(theme_change.change_type() == syncer::SyncChange::ACTION_ADD ||
theme_change.change_type() == syncer::SyncChange::ACTION_UPDATE)) {
MaybeSetTheme(current_specifics, theme_change.sync_data());
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 syncer::SyncData& sync_data) {
const sync_pb::ThemeSpecifics& theme_specifics =
sync_data.GetSpecifics().theme();
use_system_theme_by_default_ = theme_specifics.use_system_theme_by_default();
DVLOG(1) << "Set current theme from specifics: " << sync_data.ToString();
if (AreThemeSpecificsEqual(
current_specs, theme_specifics,
theme_service_->IsSystemThemeDistinctFromDefaultTheme())) {
DVLOG(1) << "Skip setting theme because specs are equal";
return ThemeSyncState::kApplied;
}
if (theme_specifics.use_custom_theme()) {
// TODO(akalin): Figure out what to do about third-party themes
// (i.e., those not on either Google gallery).
string id(theme_specifics.custom_theme_id());
GURL update_url(theme_specifics.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;
}
if (extension_service->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;
}
const auto disabled_reasons =
extensions::ExtensionPrefs::Get(profile_)->GetDisableReasons(id);
if (disabled_reasons == extensions::disable_reason::DISABLE_USER_ACTION) {
// 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 reason "
<< disabled_reasons << "; 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 (!extension_service->pending_extension_manager()->AddFromSync(
id, update_url, base::Version(), &IsTheme, kRemoteInstall)) {
LOG(WARNING) << "Could not add pending extension for " << id;
return ThemeSyncState::kFailed;
}
extension_service->CheckForUpdatesSoon();
// Return that the call triggered an extension theme installation.
return ThemeSyncState::kWaitingForExtensionInstallation;
}
if (theme_specifics.has_autogenerated_theme()) {
DVLOG(1) << "Applying autogenerated theme";
theme_service_->BuildAutogeneratedThemeFromColor(
theme_specifics.autogenerated_theme().color());
return ThemeSyncState::kApplied;
}
if (theme_specifics.use_system_theme_by_default()) {
DVLOG(1) << "Switch to use system theme";
theme_service_->UseSystemTheme();
return ThemeSyncState::kApplied;
}
DVLOG(1) << "Switch to use default theme";
theme_service_->UseDefaultTheme();
return ThemeSyncState::kApplied;
}
bool ThemeSyncableService::GetThemeSpecificsFromCurrentTheme(
sync_pb::ThemeSpecifics* theme_specifics) const {
const extensions::Extension* current_extension =
theme_service_->UsingExtensionTheme() &&
!theme_service_->UsingDefaultTheme()
? extensions::ExtensionRegistry::Get(profile_)
->enabled_extensions()
.GetByID(theme_service_->GetThemeID())
: 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;
theme_specifics->Clear();
theme_specifics->set_use_custom_theme(false);
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());
}
if (theme_service_->UsingAutogeneratedTheme()) {
// Using custom theme and it's autogenerated from color.
theme_specifics->set_use_custom_theme(false);
theme_specifics->mutable_autogenerated_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 restore it to value in sync.
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);
} else {
theme_specifics->set_use_system_theme_by_default(
use_system_theme_by_default_);
}
} else {
// Restore use_system_theme_by_default when platform doesn't distinguish
// between default theme and system theme.
theme_specifics->set_use_system_theme_by_default(
use_system_theme_by_default_);
}
return true;
}
/* static */
bool ThemeSyncableService::AreThemeSpecificsEqual(
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()) {
// 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();
} else if (a.has_autogenerated_theme()) {
return a.has_autogenerated_theme() == b.has_autogenerated_theme() &&
a.autogenerated_theme().color() == b.autogenerated_theme().color();
} else 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();
} else {
// 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_theme();
}
std::optional<syncer::ModelError> ThemeSyncableService::ProcessNewTheme(
syncer::SyncChange::SyncChangeType change_type,
const sync_pb::ThemeSpecifics& theme_specifics) {
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);
}