Home | History | Annotate | Download | only in themes
      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 
     23 bool IsTheme(const extensions::Extension* extension) {
     24   return extension->is_theme();
     25 }
     26 
     27 // TODO(akalin): Remove this.
     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 
     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 
     50 ThemeSyncableService::~ThemeSyncableService() {
     51 }
     52 
     53 void ThemeSyncableService::OnThemeChange() {
     54   if (sync_processor_.get()) {
     55     sync_pb::ThemeSpecifics current_specifics;
     56     if (!GetThemeSpecificsFromCurrentTheme(&current_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 
     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(&current_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 
    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 
    117 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 
    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(&current_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 
    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 
    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 
    255 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 */
    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 
    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