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