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