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(¤t_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(¤t_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(¤t_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