1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/themes/theme_syncable_service.h"
6
7 #include "base/strings/stringprintf.h"
8 #include "chrome/browser/extensions/extension_service.h"
9 #include "chrome/browser/profiles/profile.h"
10 #include "chrome/browser/themes/theme_service.h"
11 #include "chrome/common/extensions/manifest_url_handler.h"
12 #include "chrome/common/extensions/sync_helper.h"
13 #include "extensions/browser/extension_prefs.h"
14 #include "extensions/browser/extension_system.h"
15 #include "extensions/common/extension.h"
16 #include "sync/protocol/sync.pb.h"
17 #include "sync/protocol/theme_specifics.pb.h"
18
19 using std::string;
20
21 namespace {
22
IsTheme(const extensions::Extension * extension)23 bool IsTheme(const extensions::Extension* extension) {
24 return extension->is_theme();
25 }
26
27 // TODO(akalin): Remove this.
IsSystemThemeDistinctFromDefaultTheme()28 bool IsSystemThemeDistinctFromDefaultTheme() {
29 #if defined(TOOLKIT_GTK)
30 return true;
31 #else
32 return false;
33 #endif
34 }
35
36 } // namespace
37
38 const char ThemeSyncableService::kCurrentThemeClientTag[] = "current_theme";
39 const char ThemeSyncableService::kCurrentThemeNodeTitle[] = "Current Theme";
40
ThemeSyncableService(Profile * profile,ThemeService * theme_service)41 ThemeSyncableService::ThemeSyncableService(Profile* profile,
42 ThemeService* theme_service)
43 : profile_(profile),
44 theme_service_(theme_service),
45 use_system_theme_by_default_(false) {
46 DCHECK(profile_);
47 DCHECK(theme_service_);
48 }
49
~ThemeSyncableService()50 ThemeSyncableService::~ThemeSyncableService() {
51 }
52
OnThemeChange()53 void ThemeSyncableService::OnThemeChange() {
54 if (sync_processor_.get()) {
55 sync_pb::ThemeSpecifics current_specifics;
56 if (!GetThemeSpecificsFromCurrentTheme(¤t_specifics))
57 return; // Current theme is unsyncable.
58 ProcessNewTheme(syncer::SyncChange::ACTION_UPDATE, current_specifics);
59 use_system_theme_by_default_ =
60 current_specifics.use_system_theme_by_default();
61 }
62 }
63
MergeDataAndStartSyncing(syncer::ModelType type,const syncer::SyncDataList & initial_sync_data,scoped_ptr<syncer::SyncChangeProcessor> sync_processor,scoped_ptr<syncer::SyncErrorFactory> error_handler)64 syncer::SyncMergeResult ThemeSyncableService::MergeDataAndStartSyncing(
65 syncer::ModelType type,
66 const syncer::SyncDataList& initial_sync_data,
67 scoped_ptr<syncer::SyncChangeProcessor> sync_processor,
68 scoped_ptr<syncer::SyncErrorFactory> error_handler) {
69 DCHECK(thread_checker_.CalledOnValidThread());
70 DCHECK(!sync_processor_.get());
71 DCHECK(sync_processor.get());
72 DCHECK(error_handler.get());
73
74 syncer::SyncMergeResult merge_result(type);
75 sync_processor_ = sync_processor.Pass();
76 sync_error_handler_ = error_handler.Pass();
77
78 if (initial_sync_data.size() > 1) {
79 sync_error_handler_->CreateAndUploadError(
80 FROM_HERE,
81 base::StringPrintf("Received %d theme specifics.",
82 static_cast<int>(initial_sync_data.size())));
83 }
84
85 sync_pb::ThemeSpecifics current_specifics;
86 if (!GetThemeSpecificsFromCurrentTheme(¤t_specifics)) {
87 // Current theme is unsyncable - don't overwrite from sync data, and don't
88 // save the unsyncable theme to sync data.
89 return merge_result;
90 }
91
92 // Find the last SyncData that has theme data and set the current theme from
93 // it.
94 for (syncer::SyncDataList::const_reverse_iterator sync_data =
95 initial_sync_data.rbegin(); sync_data != initial_sync_data.rend();
96 ++sync_data) {
97 if (sync_data->GetSpecifics().has_theme()) {
98 MaybeSetTheme(current_specifics, *sync_data);
99 return merge_result;
100 }
101 }
102
103 // No theme specifics are found. Create one according to current theme.
104 merge_result.set_error(ProcessNewTheme(
105 syncer::SyncChange::ACTION_ADD, current_specifics));
106 return merge_result;
107 }
108
StopSyncing(syncer::ModelType type)109 void ThemeSyncableService::StopSyncing(syncer::ModelType type) {
110 DCHECK(thread_checker_.CalledOnValidThread());
111 DCHECK_EQ(type, syncer::THEMES);
112
113 sync_processor_.reset();
114 sync_error_handler_.reset();
115 }
116
GetAllSyncData(syncer::ModelType type) const117 syncer::SyncDataList ThemeSyncableService::GetAllSyncData(
118 syncer::ModelType type) const {
119 DCHECK(thread_checker_.CalledOnValidThread());
120 DCHECK_EQ(type, syncer::THEMES);
121
122 syncer::SyncDataList list;
123 sync_pb::EntitySpecifics entity_specifics;
124 if (GetThemeSpecificsFromCurrentTheme(entity_specifics.mutable_theme())) {
125 list.push_back(syncer::SyncData::CreateLocalData(kCurrentThemeClientTag,
126 kCurrentThemeNodeTitle,
127 entity_specifics));
128 }
129 return list;
130 }
131
ProcessSyncChanges(const tracked_objects::Location & from_here,const syncer::SyncChangeList & change_list)132 syncer::SyncError ThemeSyncableService::ProcessSyncChanges(
133 const tracked_objects::Location& from_here,
134 const syncer::SyncChangeList& change_list) {
135 DCHECK(thread_checker_.CalledOnValidThread());
136
137 if (!sync_processor_.get()) {
138 return syncer::SyncError(FROM_HERE,
139 syncer::SyncError::DATATYPE_ERROR,
140 "Theme syncable service is not started.",
141 syncer::THEMES);
142 }
143
144 // TODO(akalin): Normally, we should only have a single change and
145 // it should be an update. However, the syncapi may occasionally
146 // generates multiple changes. When we fix syncapi to not do that,
147 // we can remove the extra logic below. See:
148 // http://code.google.com/p/chromium/issues/detail?id=41696 .
149 if (change_list.size() != 1) {
150 string err_msg = base::StringPrintf("Received %d theme changes: ",
151 static_cast<int>(change_list.size()));
152 for (size_t i = 0; i < change_list.size(); ++i) {
153 base::StringAppendF(&err_msg, "[%s] ", change_list[i].ToString().c_str());
154 }
155 sync_error_handler_->CreateAndUploadError(FROM_HERE, err_msg);
156 } else if (change_list.begin()->change_type() !=
157 syncer::SyncChange::ACTION_ADD
158 && change_list.begin()->change_type() !=
159 syncer::SyncChange::ACTION_UPDATE) {
160 sync_error_handler_->CreateAndUploadError(
161 FROM_HERE,
162 "Invalid theme change: " + change_list.begin()->ToString());
163 }
164
165 sync_pb::ThemeSpecifics current_specifics;
166 if (!GetThemeSpecificsFromCurrentTheme(¤t_specifics)) {
167 // Current theme is unsyncable, so don't overwrite it.
168 return syncer::SyncError();
169 }
170
171 // Set current theme from the theme specifics of the last change of type
172 // |ACTION_ADD| or |ACTION_UPDATE|.
173 for (syncer::SyncChangeList::const_reverse_iterator theme_change =
174 change_list.rbegin(); theme_change != change_list.rend();
175 ++theme_change) {
176 if (theme_change->sync_data().GetSpecifics().has_theme() &&
177 (theme_change->change_type() == syncer::SyncChange::ACTION_ADD ||
178 theme_change->change_type() == syncer::SyncChange::ACTION_UPDATE)) {
179 MaybeSetTheme(current_specifics, theme_change->sync_data());
180 return syncer::SyncError();
181 }
182 }
183
184 return syncer::SyncError(FROM_HERE,
185 syncer::SyncError::DATATYPE_ERROR,
186 "Didn't find valid theme specifics",
187 syncer::THEMES);
188 }
189
MaybeSetTheme(const sync_pb::ThemeSpecifics & current_specs,const syncer::SyncData & sync_data)190 void ThemeSyncableService::MaybeSetTheme(
191 const sync_pb::ThemeSpecifics& current_specs,
192 const syncer::SyncData& sync_data) {
193 const sync_pb::ThemeSpecifics& sync_theme = sync_data.GetSpecifics().theme();
194 use_system_theme_by_default_ = sync_theme.use_system_theme_by_default();
195 DVLOG(1) << "Set current theme from specifics: " << sync_data.ToString();
196 if (!AreThemeSpecificsEqual(current_specs, sync_theme,
197 IsSystemThemeDistinctFromDefaultTheme())) {
198 SetCurrentThemeFromThemeSpecifics(sync_theme);
199 } else {
200 DVLOG(1) << "Skip setting theme because specs are equal";
201 }
202 }
203
SetCurrentThemeFromThemeSpecifics(const sync_pb::ThemeSpecifics & theme_specifics)204 void ThemeSyncableService::SetCurrentThemeFromThemeSpecifics(
205 const sync_pb::ThemeSpecifics& theme_specifics) {
206 if (theme_specifics.use_custom_theme()) {
207 // TODO(akalin): Figure out what to do about third-party themes
208 // (i.e., those not on either Google gallery).
209 string id(theme_specifics.custom_theme_id());
210 GURL update_url(theme_specifics.custom_theme_update_url());
211 DVLOG(1) << "Applying theme " << id << " with update_url " << update_url;
212 ExtensionService* extensions_service =
213 extensions::ExtensionSystem::Get(profile_)->extension_service();
214 CHECK(extensions_service);
215 const extensions::Extension* extension =
216 extensions_service->GetExtensionById(id, true);
217 if (extension) {
218 if (!extension->is_theme()) {
219 DVLOG(1) << "Extension " << id << " is not a theme; aborting";
220 return;
221 }
222 int disabled_reasons =
223 extensions::ExtensionPrefs::Get(profile_)->GetDisableReasons(id);
224 if (!extensions_service->IsExtensionEnabled(id) &&
225 disabled_reasons != extensions::Extension::DISABLE_USER_ACTION) {
226 DVLOG(1) << "Theme " << id << " is disabled with reason "
227 << disabled_reasons << "; aborting";
228 return;
229 }
230 // An enabled theme extension with the given id was found, so
231 // just set the current theme to it.
232 theme_service_->SetTheme(extension);
233 } else {
234 // No extension with this id exists -- we must install it; we do
235 // so by adding it as a pending extension and then triggering an
236 // auto-update cycle.
237 const bool kInstallSilently = true;
238 const bool kRemoteInstall = false;
239 if (!extensions_service->pending_extension_manager()->AddFromSync(
240 id, update_url, &IsTheme, kInstallSilently, kRemoteInstall)) {
241 LOG(WARNING) << "Could not add pending extension for " << id;
242 return;
243 }
244 extensions_service->CheckForUpdatesSoon();
245 }
246 } else if (theme_specifics.use_system_theme_by_default()) {
247 DVLOG(1) << "Switch to use system theme";
248 theme_service_->UseSystemTheme();
249 } else {
250 DVLOG(1) << "Switch to use default theme";
251 theme_service_->UseDefaultTheme();
252 }
253 }
254
GetThemeSpecificsFromCurrentTheme(sync_pb::ThemeSpecifics * theme_specifics) const255 bool ThemeSyncableService::GetThemeSpecificsFromCurrentTheme(
256 sync_pb::ThemeSpecifics* theme_specifics) const {
257 const extensions::Extension* current_theme =
258 theme_service_->UsingDefaultTheme() ?
259 NULL :
260 extensions::ExtensionSystem::Get(profile_)->extension_service()->
261 GetExtensionById(theme_service_->GetThemeID(), false);
262 if (current_theme && !extensions::sync_helper::IsSyncable(current_theme)) {
263 DVLOG(1) << "Ignoring extension from external source: " <<
264 current_theme->location();
265 return false;
266 }
267 bool use_custom_theme = (current_theme != NULL);
268 theme_specifics->set_use_custom_theme(use_custom_theme);
269 if (IsSystemThemeDistinctFromDefaultTheme()) {
270 // On platform where system theme is different from default theme, set
271 // use_system_theme_by_default to true if system theme is used, false
272 // if default system theme is used. Otherwise restore it to value in sync.
273 if (theme_service_->UsingSystemTheme()) {
274 theme_specifics->set_use_system_theme_by_default(true);
275 } else if (theme_service_->UsingDefaultTheme()) {
276 theme_specifics->set_use_system_theme_by_default(false);
277 } else {
278 theme_specifics->set_use_system_theme_by_default(
279 use_system_theme_by_default_);
280 }
281 } else {
282 // Restore use_system_theme_by_default when platform doesn't distinguish
283 // between default theme and system theme.
284 theme_specifics->set_use_system_theme_by_default(
285 use_system_theme_by_default_);
286 }
287
288 if (use_custom_theme) {
289 DCHECK(current_theme);
290 DCHECK(current_theme->is_theme());
291 theme_specifics->set_custom_theme_name(current_theme->name());
292 theme_specifics->set_custom_theme_id(current_theme->id());
293 theme_specifics->set_custom_theme_update_url(
294 extensions::ManifestURL::GetUpdateURL(current_theme).spec());
295 } else {
296 DCHECK(!current_theme);
297 theme_specifics->clear_custom_theme_name();
298 theme_specifics->clear_custom_theme_id();
299 theme_specifics->clear_custom_theme_update_url();
300 }
301 return true;
302 }
303
304 /* static */
AreThemeSpecificsEqual(const sync_pb::ThemeSpecifics & a,const sync_pb::ThemeSpecifics & b,bool is_system_theme_distinct_from_default_theme)305 bool ThemeSyncableService::AreThemeSpecificsEqual(
306 const sync_pb::ThemeSpecifics& a,
307 const sync_pb::ThemeSpecifics& b,
308 bool is_system_theme_distinct_from_default_theme) {
309 if (a.use_custom_theme() != b.use_custom_theme()) {
310 return false;
311 }
312
313 if (a.use_custom_theme()) {
314 // We're using a custom theme, so simply compare IDs since those
315 // are guaranteed unique.
316 return a.custom_theme_id() == b.custom_theme_id();
317 } else if (is_system_theme_distinct_from_default_theme) {
318 // We're not using a custom theme, but we care about system
319 // vs. default.
320 return a.use_system_theme_by_default() == b.use_system_theme_by_default();
321 } else {
322 // We're not using a custom theme, and we don't care about system
323 // vs. default.
324 return true;
325 }
326 }
327
ProcessNewTheme(syncer::SyncChange::SyncChangeType change_type,const sync_pb::ThemeSpecifics & theme_specifics)328 syncer::SyncError ThemeSyncableService::ProcessNewTheme(
329 syncer::SyncChange::SyncChangeType change_type,
330 const sync_pb::ThemeSpecifics& theme_specifics) {
331 syncer::SyncChangeList changes;
332 sync_pb::EntitySpecifics entity_specifics;
333 entity_specifics.mutable_theme()->CopyFrom(theme_specifics);
334
335 changes.push_back(
336 syncer::SyncChange(FROM_HERE, change_type,
337 syncer::SyncData::CreateLocalData(
338 kCurrentThemeClientTag, kCurrentThemeNodeTitle,
339 entity_specifics)));
340
341 DVLOG(1) << "Update theme specifics from current theme: "
342 << changes.back().ToString();
343
344 return sync_processor_->ProcessSyncChanges(FROM_HERE, changes);
345 }
346