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_service.h" 6 7 #include "base/bind.h" 8 #include "base/memory/ref_counted_memory.h" 9 #include "base/prefs/pref_service.h" 10 #include "base/sequenced_task_runner.h" 11 #include "base/strings/string_util.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "chrome/browser/chrome_notification_types.h" 14 #include "chrome/browser/extensions/extension_service.h" 15 #include "chrome/browser/extensions/extension_system.h" 16 #include "chrome/browser/managed_mode/managed_user_service.h" 17 #include "chrome/browser/managed_mode/managed_user_service_factory.h" 18 #include "chrome/browser/managed_mode/managed_user_theme.h" 19 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/themes/browser_theme_pack.h" 21 #include "chrome/browser/themes/custom_theme_supplier.h" 22 #include "chrome/browser/themes/theme_properties.h" 23 #include "chrome/browser/themes/theme_syncable_service.h" 24 #include "chrome/common/chrome_constants.h" 25 #include "chrome/common/extensions/extension_manifest_constants.h" 26 #include "chrome/common/pref_names.h" 27 #include "content/public/browser/notification_service.h" 28 #include "content/public/browser/user_metrics.h" 29 #include "grit/theme_resources.h" 30 #include "grit/ui_resources.h" 31 #include "ui/base/layout.h" 32 #include "ui/base/resource/resource_bundle.h" 33 #include "ui/gfx/image/image_skia.h" 34 35 #if defined(OS_WIN) 36 #include "ui/base/win/shell.h" 37 #endif 38 39 using content::BrowserThread; 40 using content::UserMetricsAction; 41 using extensions::Extension; 42 using ui::ResourceBundle; 43 44 typedef ThemeProperties Properties; 45 46 // The default theme if we haven't installed a theme yet or if we've clicked 47 // the "Use Classic" button. 48 const char* ThemeService::kDefaultThemeID = ""; 49 50 namespace { 51 52 // The default theme if we've gone to the theme gallery and installed the 53 // "Default" theme. We have to detect this case specifically. (By the time we 54 // realize we've installed the default theme, we already have an extension 55 // unpacked on the filesystem.) 56 const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn"; 57 58 SkColor TintForUnderline(SkColor input) { 59 return SkColorSetA(input, SkColorGetA(input) / 3); 60 } 61 62 SkColor IncreaseLightness(SkColor color, double percent) { 63 color_utils::HSL result; 64 color_utils::SkColorToHSL(color, &result); 65 result.l += (1 - result.l) * percent; 66 return color_utils::HSLToSkColor(result, SkColorGetA(color)); 67 } 68 69 // Writes the theme pack to disk on a separate thread. 70 void WritePackToDiskCallback(BrowserThemePack* pack, 71 const base::FilePath& path) { 72 if (!pack->WriteToDisk(path)) 73 NOTREACHED() << "Could not write theme pack to disk"; 74 } 75 76 } // namespace 77 78 ThemeService::ThemeService() 79 : ready_(false), 80 rb_(ResourceBundle::GetSharedInstance()), 81 profile_(NULL), 82 number_of_infobars_(0), 83 weak_ptr_factory_(this) { 84 } 85 86 ThemeService::~ThemeService() { 87 FreePlatformCaches(); 88 } 89 90 void ThemeService::Init(Profile* profile) { 91 DCHECK(CalledOnValidThread()); 92 profile_ = profile; 93 94 ManagedUserServiceFactory::GetForProfile(profile)->AddInitCallback(base::Bind( 95 &ThemeService::OnManagedUserInitialized, weak_ptr_factory_.GetWeakPtr())); 96 97 LoadThemePrefs(); 98 99 if (!ready_) { 100 registrar_.Add(this, 101 chrome::NOTIFICATION_EXTENSIONS_READY, 102 content::Source<Profile>(profile_)); 103 } 104 105 theme_syncable_service_.reset(new ThemeSyncableService(profile_, this)); 106 } 107 108 gfx::Image ThemeService::GetImageNamed(int id) const { 109 DCHECK(CalledOnValidThread()); 110 111 gfx::Image image; 112 if (theme_supplier_.get()) 113 image = theme_supplier_->GetImageNamed(id); 114 115 if (image.IsEmpty()) 116 image = rb_.GetNativeImageNamed(id); 117 118 return image; 119 } 120 121 gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const { 122 gfx::Image image = GetImageNamed(id); 123 if (image.IsEmpty()) 124 return NULL; 125 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns 126 // its images const. GetImageSkiaNamed() also should but has many callsites. 127 return const_cast<gfx::ImageSkia*>(image.ToImageSkia()); 128 } 129 130 SkColor ThemeService::GetColor(int id) const { 131 DCHECK(CalledOnValidThread()); 132 SkColor color; 133 if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color)) 134 return color; 135 136 // For backward compat with older themes, some newer colors are generated from 137 // older ones if they are missing. 138 switch (id) { 139 case Properties::COLOR_NTP_SECTION_HEADER_TEXT: 140 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30); 141 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER: 142 return GetColor(Properties::COLOR_NTP_TEXT); 143 case Properties::COLOR_NTP_SECTION_HEADER_RULE: 144 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70); 145 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT: 146 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86); 147 case Properties::COLOR_NTP_TEXT_LIGHT: 148 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40); 149 case Properties::COLOR_MANAGED_USER_LABEL: 150 return color_utils::GetReadableColor( 151 SK_ColorWHITE, 152 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND)); 153 case Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND: 154 return color_utils::BlendTowardOppositeLuminance( 155 GetColor(Properties::COLOR_FRAME), 0x80); 156 case Properties::COLOR_MANAGED_USER_LABEL_BORDER: 157 return color_utils::AlphaBlend( 158 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND), 159 SK_ColorBLACK, 160 230); 161 } 162 163 return Properties::GetDefaultColor(id); 164 } 165 166 bool ThemeService::GetDisplayProperty(int id, int* result) const { 167 if (theme_supplier_.get()) 168 return theme_supplier_->GetDisplayProperty(id, result); 169 170 return Properties::GetDefaultDisplayProperty(id, result); 171 } 172 173 bool ThemeService::ShouldUseNativeFrame() const { 174 if (HasCustomImage(IDR_THEME_FRAME)) 175 return false; 176 #if defined(OS_WIN) 177 return ui::win::IsAeroGlassEnabled(); 178 #else 179 return false; 180 #endif 181 } 182 183 bool ThemeService::HasCustomImage(int id) const { 184 if (!Properties::IsThemeableImage(id)) 185 return false; 186 187 if (theme_supplier_.get()) 188 return theme_supplier_->HasCustomImage(id); 189 190 return false; 191 } 192 193 base::RefCountedMemory* ThemeService::GetRawData( 194 int id, 195 ui::ScaleFactor scale_factor) const { 196 // Check to see whether we should substitute some images. 197 int ntp_alternate; 198 GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE, &ntp_alternate); 199 if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0) 200 id = IDR_PRODUCT_LOGO_WHITE; 201 202 base::RefCountedMemory* data = NULL; 203 if (theme_supplier_.get()) 204 data = theme_supplier_->GetRawData(id, scale_factor); 205 if (!data) 206 data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P); 207 208 return data; 209 } 210 211 void ThemeService::Observe(int type, 212 const content::NotificationSource& source, 213 const content::NotificationDetails& details) { 214 DCHECK(type == chrome::NOTIFICATION_EXTENSIONS_READY); 215 registrar_.Remove(this, chrome::NOTIFICATION_EXTENSIONS_READY, 216 content::Source<Profile>(profile_)); 217 218 MigrateTheme(); 219 set_ready(); 220 221 // Send notification in case anyone requested data and cached it when the 222 // theme service was not ready yet. 223 NotifyThemeChanged(); 224 } 225 226 void ThemeService::SetTheme(const Extension* extension) { 227 // Clear our image cache. 228 FreePlatformCaches(); 229 230 DCHECK(extension); 231 DCHECK(extension->is_theme()); 232 if (DCHECK_IS_ON()) { 233 ExtensionService* service = 234 extensions::ExtensionSystem::Get(profile_)->extension_service(); 235 DCHECK(service); 236 DCHECK(service->GetExtensionById(extension->id(), false)); 237 } 238 239 BuildFromExtension(extension); 240 SaveThemeID(extension->id()); 241 242 NotifyThemeChanged(); 243 content::RecordAction(UserMetricsAction("Themes_Installed")); 244 } 245 246 void ThemeService::SetCustomDefaultTheme( 247 scoped_refptr<CustomThemeSupplier> theme_supplier) { 248 ClearAllThemeData(); 249 SwapThemeSupplier(theme_supplier); 250 NotifyThemeChanged(); 251 } 252 253 bool ThemeService::ShouldInitWithNativeTheme() const { 254 return false; 255 } 256 257 void ThemeService::RemoveUnusedThemes() { 258 // We do not want to garbage collect themes on startup (|ready_| is false). 259 // Themes will get garbage collected once 260 // ExtensionService::GarbageCollectExtensions() runs. 261 if (!profile_ || !ready_) 262 return; 263 264 ExtensionService* service = profile_->GetExtensionService(); 265 if (!service) 266 return; 267 std::string current_theme = GetThemeID(); 268 std::vector<std::string> remove_list; 269 const ExtensionSet* extensions = service->extensions(); 270 for (ExtensionSet::const_iterator it = extensions->begin(); 271 it != extensions->end(); ++it) { 272 if ((*it)->is_theme() && (*it)->id() != current_theme) { 273 remove_list.push_back((*it)->id()); 274 } 275 } 276 for (size_t i = 0; i < remove_list.size(); ++i) 277 service->UninstallExtension(remove_list[i], false, NULL); 278 } 279 280 void ThemeService::UseDefaultTheme() { 281 if (ready_) 282 content::RecordAction(UserMetricsAction("Themes_Reset")); 283 if (IsManagedUser()) { 284 SetManagedUserTheme(); 285 return; 286 } 287 ClearAllThemeData(); 288 NotifyThemeChanged(); 289 } 290 291 void ThemeService::SetNativeTheme() { 292 UseDefaultTheme(); 293 } 294 295 bool ThemeService::UsingDefaultTheme() const { 296 std::string id = GetThemeID(); 297 return id == ThemeService::kDefaultThemeID || 298 (id == kDefaultThemeGalleryID && !IsManagedUser()); 299 } 300 301 bool ThemeService::UsingNativeTheme() const { 302 return UsingDefaultTheme(); 303 } 304 305 std::string ThemeService::GetThemeID() const { 306 return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID); 307 } 308 309 color_utils::HSL ThemeService::GetTint(int id) const { 310 DCHECK(CalledOnValidThread()); 311 312 color_utils::HSL hsl; 313 if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl)) 314 return hsl; 315 316 return ThemeProperties::GetDefaultTint(id); 317 } 318 319 void ThemeService::ClearAllThemeData() { 320 if (!ready_) 321 return; 322 323 SwapThemeSupplier(NULL); 324 325 // Clear our image cache. 326 FreePlatformCaches(); 327 328 profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename); 329 SaveThemeID(kDefaultThemeID); 330 331 RemoveUnusedThemes(); 332 } 333 334 void ThemeService::LoadThemePrefs() { 335 PrefService* prefs = profile_->GetPrefs(); 336 337 std::string current_id = GetThemeID(); 338 if (current_id == kDefaultThemeID) { 339 // Managed users have a different default theme. 340 if (IsManagedUser()) 341 SetManagedUserTheme(); 342 else if (ShouldInitWithNativeTheme()) 343 SetNativeTheme(); 344 else 345 UseDefaultTheme(); 346 set_ready(); 347 return; 348 } 349 350 bool loaded_pack = false; 351 352 // If we don't have a file pack, we're updating from an old version. 353 base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename); 354 if (path != base::FilePath()) { 355 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id)); 356 loaded_pack = theme_supplier_.get() != NULL; 357 } 358 359 if (loaded_pack) { 360 content::RecordAction(UserMetricsAction("Themes.Loaded")); 361 set_ready(); 362 } else { 363 // TODO(erg): We need to pop up a dialog informing the user that their 364 // theme is being migrated. 365 ExtensionService* service = 366 extensions::ExtensionSystem::Get(profile_)->extension_service(); 367 if (service && service->is_ready()) { 368 MigrateTheme(); 369 set_ready(); 370 } 371 } 372 } 373 374 void ThemeService::NotifyThemeChanged() { 375 if (!ready_) 376 return; 377 378 DVLOG(1) << "Sending BROWSER_THEME_CHANGED"; 379 // Redraw! 380 content::NotificationService* service = 381 content::NotificationService::current(); 382 service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 383 content::Source<ThemeService>(this), 384 content::NotificationService::NoDetails()); 385 #if defined(OS_MACOSX) 386 NotifyPlatformThemeChanged(); 387 #endif // OS_MACOSX 388 389 // Notify sync that theme has changed. 390 if (theme_syncable_service_.get()) { 391 theme_syncable_service_->OnThemeChange(); 392 } 393 } 394 395 #if defined(OS_WIN) || defined(USE_AURA) 396 void ThemeService::FreePlatformCaches() { 397 // Views (Skia) has no platform image cache to clear. 398 } 399 #endif 400 401 void ThemeService::SwapThemeSupplier( 402 scoped_refptr<CustomThemeSupplier> theme_supplier) { 403 if (theme_supplier_.get()) 404 theme_supplier_->StopUsingTheme(); 405 theme_supplier_ = theme_supplier; 406 if (theme_supplier_.get()) 407 theme_supplier_->StartUsingTheme(); 408 } 409 410 void ThemeService::MigrateTheme() { 411 ExtensionService* service = 412 extensions::ExtensionSystem::Get(profile_)->extension_service(); 413 const Extension* extension = service ? 414 service->GetExtensionById(GetThemeID(), false) : NULL; 415 if (extension) { 416 DLOG(ERROR) << "Migrating theme"; 417 BuildFromExtension(extension); 418 content::RecordAction(UserMetricsAction("Themes.Migrated")); 419 } else { 420 DLOG(ERROR) << "Theme is mysteriously gone."; 421 ClearAllThemeData(); 422 content::RecordAction(UserMetricsAction("Themes.Gone")); 423 } 424 } 425 426 void ThemeService::SavePackName(const base::FilePath& pack_path) { 427 profile_->GetPrefs()->SetFilePath( 428 prefs::kCurrentThemePackFilename, pack_path); 429 } 430 431 void ThemeService::SaveThemeID(const std::string& id) { 432 profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id); 433 } 434 435 void ThemeService::BuildFromExtension(const Extension* extension) { 436 scoped_refptr<BrowserThemePack> pack( 437 BrowserThemePack::BuildFromExtension(extension)); 438 if (!pack.get()) { 439 // TODO(erg): We've failed to install the theme; perhaps we should tell the 440 // user? http://crbug.com/34780 441 LOG(ERROR) << "Could not load theme."; 442 return; 443 } 444 445 ExtensionService* service = 446 extensions::ExtensionSystem::Get(profile_)->extension_service(); 447 if (!service) 448 return; 449 450 // Write the packed file to disk. 451 base::FilePath pack_path = 452 extension->path().Append(chrome::kThemePackFilename); 453 service->GetFileTaskRunner()->PostTask( 454 FROM_HERE, 455 base::Bind(&WritePackToDiskCallback, pack, pack_path)); 456 457 SavePackName(pack_path); 458 SwapThemeSupplier(pack); 459 } 460 461 bool ThemeService::IsManagedUser() const { 462 return profile_->IsManaged(); 463 } 464 465 void ThemeService::SetManagedUserTheme() { 466 SetCustomDefaultTheme(new ManagedUserTheme); 467 } 468 469 void ThemeService::OnManagedUserInitialized() { 470 // Currently when creating a supervised user, the ThemeService is initialized 471 // before the boolean flag indicating the profile belongs to a supervised 472 // user gets set. In order to get the custom managed user theme, we get a 473 // callback when ManagedUserService is initialized, which happens some time 474 // after the boolean flag has been set in 475 // ProfileManager::InitProfileUserPrefs() and after the 476 // NOTIFICATION_EXTENSIONS_READY notification is sent. 477 if ((theme_supplier_.get() && 478 (theme_supplier_->get_theme_type() == CustomThemeSupplier::EXTENSION || 479 theme_supplier_->get_theme_type() == 480 CustomThemeSupplier::MANAGED_USER_THEME)) || 481 !IsManagedUser()) { 482 return; 483 } 484 485 SetManagedUserTheme(); 486 } 487 488 void ThemeService::OnInfobarDisplayed() { 489 number_of_infobars_++; 490 } 491 492 void ThemeService::OnInfobarDestroyed() { 493 number_of_infobars_--; 494 495 if (number_of_infobars_ == 0) 496 RemoveUnusedThemes(); 497 } 498 499 ThemeSyncableService* ThemeService::GetThemeSyncableService() const { 500 return theme_syncable_service_.get(); 501 } 502