// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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 "base/strings/stringprintf.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_system.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/common/extensions/manifest_url_handler.h"
#include "chrome/common/extensions/sync_helper.h"
#include "extensions/common/extension.h"
#include "sync/protocol/sync.pb.h"
#include "sync/protocol/theme_specifics.pb.h"
using std::string;
namespace {
bool IsTheme(const extensions::Extension* extension) {
return extension->is_theme();
}
// TODO(akalin): Remove this.
bool IsSystemThemeDistinctFromDefaultTheme() {
#if defined(TOOLKIT_GTK)
return true;
#else
return false;
#endif
}
} // namespace
const char ThemeSyncableService::kCurrentThemeClientTag[] = "current_theme";
const char ThemeSyncableService::kCurrentThemeNodeTitle[] = "Current Theme";
ThemeSyncableService::ThemeSyncableService(Profile* profile,
ThemeService* theme_service)
: profile_(profile),
theme_service_(theme_service),
use_system_theme_by_default_(false) {
DCHECK(profile_);
DCHECK(theme_service_);
}
ThemeSyncableService::~ThemeSyncableService() {
}
void ThemeSyncableService::OnThemeChange() {
if (sync_processor_.get()) {
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(¤t_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();
}
}
syncer::SyncMergeResult ThemeSyncableService::MergeDataAndStartSyncing(
syncer::ModelType type,
const syncer::SyncDataList& initial_sync_data,
scoped_ptr<syncer::SyncChangeProcessor> sync_processor,
scoped_ptr<syncer::SyncErrorFactory> error_handler) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!sync_processor_.get());
DCHECK(sync_processor.get());
DCHECK(error_handler.get());
syncer::SyncMergeResult merge_result(type);
sync_processor_ = sync_processor.Pass();
sync_error_handler_ = error_handler.Pass();
if (initial_sync_data.size() > 1) {
sync_error_handler_->CreateAndUploadError(
FROM_HERE,
base::StringPrintf("Received %d theme specifics.",
static_cast<int>(initial_sync_data.size())));
}
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(¤t_specifics)) {
// Current theme is unsyncable - don't overwrite from sync data, and don't
// save the unsyncable theme to sync data.
return merge_result;
}
// Find the last SyncData that has theme data and set the current theme from
// it.
for (syncer::SyncDataList::const_reverse_iterator sync_data =
initial_sync_data.rbegin(); sync_data != initial_sync_data.rend();
++sync_data) {
if (sync_data->GetSpecifics().has_theme()) {
MaybeSetTheme(current_specifics, *sync_data);
return merge_result;
}
}
// No theme specifics are found. Create one according to current theme.
merge_result.set_error(ProcessNewTheme(
syncer::SyncChange::ACTION_ADD, current_specifics));
return merge_result;
}
void ThemeSyncableService::StopSyncing(syncer::ModelType type) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(type, syncer::THEMES);
sync_processor_.reset();
sync_error_handler_.reset();
}
syncer::SyncDataList ThemeSyncableService::GetAllSyncData(
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(kCurrentThemeClientTag,
kCurrentThemeNodeTitle,
entity_specifics));
}
return list;
}
syncer::SyncError ThemeSyncableService::ProcessSyncChanges(
const tracked_objects::Location& from_here,
const syncer::SyncChangeList& change_list) {
DCHECK(thread_checker_.CalledOnValidThread());
if (!sync_processor_.get()) {
return syncer::SyncError(FROM_HERE,
syncer::SyncError::DATATYPE_ERROR,
"Theme syncable service is not started.",
syncer::THEMES);
}
// 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());
}
sync_error_handler_->CreateAndUploadError(FROM_HERE, err_msg);
} else if (change_list.begin()->change_type() !=
syncer::SyncChange::ACTION_ADD
&& change_list.begin()->change_type() !=
syncer::SyncChange::ACTION_UPDATE) {
sync_error_handler_->CreateAndUploadError(
FROM_HERE,
"Invalid theme change: " + change_list.begin()->ToString());
}
sync_pb::ThemeSpecifics current_specifics;
if (!GetThemeSpecificsFromCurrentTheme(¤t_specifics)) {
// Current theme is unsyncable, so don't overwrite it.
return syncer::SyncError();
}
// Set current theme from the theme specifics of the last change of type
// |ACTION_ADD| or |ACTION_UPDATE|.
for (syncer::SyncChangeList::const_reverse_iterator theme_change =
change_list.rbegin(); theme_change != change_list.rend();
++theme_change) {
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 syncer::SyncError();
}
}
return syncer::SyncError(FROM_HERE,
syncer::SyncError::DATATYPE_ERROR,
"Didn't find valid theme specifics",
syncer::THEMES);
}
void ThemeSyncableService::MaybeSetTheme(
const sync_pb::ThemeSpecifics& current_specs,
const syncer::SyncData& sync_data) {
const sync_pb::ThemeSpecifics& sync_theme = sync_data.GetSpecifics().theme();
use_system_theme_by_default_ = sync_theme.use_system_theme_by_default();
DVLOG(1) << "Set current theme from specifics: " << sync_data.ToString();
if (!AreThemeSpecificsEqual(current_specs, sync_theme,
IsSystemThemeDistinctFromDefaultTheme())) {
SetCurrentThemeFromThemeSpecifics(sync_theme);
} else {
DVLOG(1) << "Skip setting theme because specs are equal";
}
}
void ThemeSyncableService::SetCurrentThemeFromThemeSpecifics(
const sync_pb::ThemeSpecifics& theme_specifics) {
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;
ExtensionService* extensions_service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
CHECK(extensions_service);
const extensions::Extension* extension =
extensions_service->GetExtensionById(id, true);
if (extension) {
if (!extension->is_theme()) {
DVLOG(1) << "Extension " << id << " is not a theme; aborting";
return;
}
int disabled_reasons =
extensions_service->extension_prefs()->GetDisableReasons(id);
if (!extensions_service->IsExtensionEnabled(id) &&
disabled_reasons != extensions::Extension::DISABLE_USER_ACTION) {
DVLOG(1) << "Theme " << id << " is disabled with reason "
<< disabled_reasons << "; aborting";
return;
}
// An enabled theme extension with the given id was found, so
// just set the current theme to it.
theme_service_->SetTheme(extension);
} else {
// 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 kInstallSilently = true;
if (!extensions_service->pending_extension_manager()->AddFromSync(
id, update_url, &IsTheme, kInstallSilently)) {
LOG(WARNING) << "Could not add pending extension for " << id;
return;
}
extensions_service->CheckForUpdatesSoon();
}
} else if (theme_specifics.use_system_theme_by_default()) {
DVLOG(1) << "Switch to use native theme";
theme_service_->SetNativeTheme();
} else {
DVLOG(1) << "Switch to use default theme";
theme_service_->UseDefaultTheme();
}
}
bool ThemeSyncableService::GetThemeSpecificsFromCurrentTheme(
sync_pb::ThemeSpecifics* theme_specifics) const {
const extensions::Extension* current_theme =
theme_service_->UsingDefaultTheme() ?
NULL :
extensions::ExtensionSystem::Get(profile_)->extension_service()->
GetExtensionById(theme_service_->GetThemeID(), false);
if (current_theme && !extensions::sync_helper::IsSyncable(current_theme)) {
DVLOG(1) << "Ignoring extension from external source: " <<
current_theme->location();
return false;
}
bool use_custom_theme = (current_theme != NULL);
theme_specifics->set_use_custom_theme(use_custom_theme);
if (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_->UsingNativeTheme()) {
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_);
}
if (use_custom_theme) {
DCHECK(current_theme);
DCHECK(current_theme->is_theme());
theme_specifics->set_custom_theme_name(current_theme->name());
theme_specifics->set_custom_theme_id(current_theme->id());
theme_specifics->set_custom_theme_update_url(
extensions::ManifestURL::GetUpdateURL(current_theme).spec());
} else {
DCHECK(!current_theme);
theme_specifics->clear_custom_theme_name();
theme_specifics->clear_custom_theme_id();
theme_specifics->clear_custom_theme_update_url();
}
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 (a.use_custom_theme() != b.use_custom_theme()) {
return false;
}
if (a.use_custom_theme()) {
// We're using a custom theme, so simply compare IDs since those
// are guaranteed unique.
return a.custom_theme_id() == b.custom_theme_id();
} 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;
}
}
syncer::SyncError 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.push_back(
syncer::SyncChange(FROM_HERE, change_type,
syncer::SyncData::CreateLocalData(
kCurrentThemeClientTag, kCurrentThemeNodeTitle,
entity_specifics)));
DVLOG(1) << "Update theme specifics from current theme: "
<< changes.back().ToString();
return sync_processor_->ProcessSyncChanges(FROM_HERE, changes);
}