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/manifest_url_handler.h" 13 #include "chrome/common/extensions/sync_helper.h" 14 #include "extensions/common/extension.h" 15 #include "sync/protocol/sync.pb.h" 16 #include "sync/protocol/theme_specifics.pb.h" 17 18 using std::string; 19 20 namespace { 21 22 bool IsTheme(const extensions::Extension* extension) { 23 return extension->is_theme(); 24 } 25 26 // TODO(akalin): Remove this. 27 bool IsSystemThemeDistinctFromDefaultTheme() { 28 #if defined(TOOLKIT_GTK) 29 return true; 30 #else 31 return false; 32 #endif 33 } 34 35 } // namespace 36 37 const char ThemeSyncableService::kCurrentThemeClientTag[] = "current_theme"; 38 const char ThemeSyncableService::kCurrentThemeNodeTitle[] = "Current Theme"; 39 40 ThemeSyncableService::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 49 ThemeSyncableService::~ThemeSyncableService() { 50 } 51 52 void ThemeSyncableService::OnThemeChange() { 53 if (sync_processor_.get()) { 54 sync_pb::ThemeSpecifics current_specifics; 55 if (!GetThemeSpecificsFromCurrentTheme(¤t_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 63 syncer::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(¤t_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 108 void 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 116 syncer::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 131 syncer::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(¤t_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 189 void 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 203 void 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 253 bool 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 */ 303 bool 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 326 syncer::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