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