theme_service.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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/message_loop/message_loop.h" 10#include "base/prefs/pref_service.h" 11#include "base/sequenced_task_runner.h" 12#include "base/strings/string_util.h" 13#include "base/strings/utf_string_conversions.h" 14#include "chrome/browser/chrome_notification_types.h" 15#include "chrome/browser/extensions/extension_service.h" 16#include "chrome/browser/managed_mode/managed_user_theme.h" 17#include "chrome/browser/profiles/profile.h" 18#include "chrome/browser/themes/browser_theme_pack.h" 19#include "chrome/browser/themes/custom_theme_supplier.h" 20#include "chrome/browser/themes/theme_properties.h" 21#include "chrome/browser/themes/theme_syncable_service.h" 22#include "chrome/common/chrome_constants.h" 23#include "chrome/common/pref_names.h" 24#include "content/public/browser/notification_service.h" 25#include "content/public/browser/user_metrics.h" 26#include "extensions/browser/extension_prefs.h" 27#include "extensions/browser/extension_registry.h" 28#include "extensions/browser/extension_system.h" 29#include "extensions/common/extension.h" 30#include "extensions/common/extension_set.h" 31#include "grit/theme_resources.h" 32#include "grit/ui_resources.h" 33#include "ui/base/layout.h" 34#include "ui/base/resource/resource_bundle.h" 35#include "ui/gfx/image/image_skia.h" 36 37#if defined(OS_WIN) 38#include "ui/base/win/shell.h" 39#endif 40 41using base::UserMetricsAction; 42using content::BrowserThread; 43using extensions::Extension; 44using extensions::UnloadedExtensionInfo; 45using ui::ResourceBundle; 46 47typedef ThemeProperties Properties; 48 49// The default theme if we haven't installed a theme yet or if we've clicked 50// the "Use Classic" button. 51const char* ThemeService::kDefaultThemeID = ""; 52 53namespace { 54 55// The default theme if we've gone to the theme gallery and installed the 56// "Default" theme. We have to detect this case specifically. (By the time we 57// realize we've installed the default theme, we already have an extension 58// unpacked on the filesystem.) 59const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn"; 60 61// Wait this many seconds after startup to garbage collect unused themes. 62// Removing unused themes is done after a delay because there is no 63// reason to do it at startup. 64// ExtensionService::GarbageCollectExtensions() does something similar. 65const int kRemoveUnusedThemesStartupDelay = 30; 66 67SkColor IncreaseLightness(SkColor color, double percent) { 68 color_utils::HSL result; 69 color_utils::SkColorToHSL(color, &result); 70 result.l += (1 - result.l) * percent; 71 return color_utils::HSLToSkColor(result, SkColorGetA(color)); 72} 73 74// Writes the theme pack to disk on a separate thread. 75void WritePackToDiskCallback(BrowserThemePack* pack, 76 const base::FilePath& path) { 77 if (!pack->WriteToDisk(path)) 78 NOTREACHED() << "Could not write theme pack to disk"; 79} 80 81} // namespace 82 83ThemeService::ThemeService() 84 : ready_(false), 85 rb_(ResourceBundle::GetSharedInstance()), 86 profile_(NULL), 87 installed_pending_load_id_(kDefaultThemeID), 88 number_of_infobars_(0), 89 weak_ptr_factory_(this) { 90} 91 92ThemeService::~ThemeService() { 93 FreePlatformCaches(); 94} 95 96void ThemeService::Init(Profile* profile) { 97 DCHECK(CalledOnValidThread()); 98 profile_ = profile; 99 100 LoadThemePrefs(); 101 102 registrar_.Add(this, 103 chrome::NOTIFICATION_EXTENSIONS_READY, 104 content::Source<Profile>(profile_)); 105 106 theme_syncable_service_.reset(new ThemeSyncableService(profile_, this)); 107} 108 109gfx::Image ThemeService::GetImageNamed(int id) const { 110 DCHECK(CalledOnValidThread()); 111 112 gfx::Image image; 113 if (theme_supplier_.get()) 114 image = theme_supplier_->GetImageNamed(id); 115 116 if (image.IsEmpty()) 117 image = rb_.GetNativeImageNamed(id); 118 119 return image; 120} 121 122bool ThemeService::UsingSystemTheme() const { 123 return UsingDefaultTheme(); 124} 125 126gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const { 127 gfx::Image image = GetImageNamed(id); 128 if (image.IsEmpty()) 129 return NULL; 130 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns 131 // its images const. GetImageSkiaNamed() also should but has many callsites. 132 return const_cast<gfx::ImageSkia*>(image.ToImageSkia()); 133} 134 135SkColor ThemeService::GetColor(int id) const { 136 DCHECK(CalledOnValidThread()); 137 SkColor color; 138 if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color)) 139 return color; 140 141 // For backward compat with older themes, some newer colors are generated from 142 // older ones if they are missing. 143 switch (id) { 144 case Properties::COLOR_NTP_SECTION_HEADER_TEXT: 145 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30); 146 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER: 147 return GetColor(Properties::COLOR_NTP_TEXT); 148 case Properties::COLOR_NTP_SECTION_HEADER_RULE: 149 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70); 150 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT: 151 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86); 152 case Properties::COLOR_NTP_TEXT_LIGHT: 153 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40); 154 case Properties::COLOR_MANAGED_USER_LABEL: 155 return color_utils::GetReadableColor( 156 SK_ColorWHITE, 157 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND)); 158 case Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND: 159 return color_utils::BlendTowardOppositeLuminance( 160 GetColor(Properties::COLOR_FRAME), 0x80); 161 case Properties::COLOR_MANAGED_USER_LABEL_BORDER: 162 return color_utils::AlphaBlend( 163 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND), 164 SK_ColorBLACK, 165 230); 166 case Properties::COLOR_STATUS_BAR_TEXT: { 167 // A long time ago, we blended the toolbar and the tab text together to 168 // get the status bar text because, at the time, our text rendering in 169 // views couldn't do alpha blending. Even though this is no longer the 170 // case, this blending decision is built into the majority of themes that 171 // exist, and we must keep doing it. 172 SkColor toolbar_color = GetColor(Properties::COLOR_TOOLBAR); 173 SkColor text_color = GetColor(Properties::COLOR_TAB_TEXT); 174 return SkColorSetARGB( 175 SkColorGetA(text_color), 176 (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2, 177 (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2, 178 (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2); 179 } 180 } 181 182 return Properties::GetDefaultColor(id); 183} 184 185int ThemeService::GetDisplayProperty(int id) const { 186 int result = 0; 187 if (theme_supplier_.get() && 188 theme_supplier_->GetDisplayProperty(id, &result)) { 189 return result; 190 } 191 192 if (id == Properties::NTP_LOGO_ALTERNATE && 193 !UsingDefaultTheme() && 194 !UsingSystemTheme()) { 195 // Use the alternate logo for themes from the web store except for 196 // |kDefaultThemeGalleryID|. 197 return 1; 198 } 199 200 return Properties::GetDefaultDisplayProperty(id); 201} 202 203bool ThemeService::ShouldUseNativeFrame() const { 204 if (HasCustomImage(IDR_THEME_FRAME)) 205 return false; 206#if defined(OS_WIN) 207 return ui::win::IsAeroGlassEnabled(); 208#else 209 return false; 210#endif 211} 212 213bool ThemeService::HasCustomImage(int id) const { 214 if (!Properties::IsThemeableImage(id)) 215 return false; 216 217 if (theme_supplier_.get()) 218 return theme_supplier_->HasCustomImage(id); 219 220 return false; 221} 222 223base::RefCountedMemory* ThemeService::GetRawData( 224 int id, 225 ui::ScaleFactor scale_factor) const { 226 // Check to see whether we should substitute some images. 227 int ntp_alternate = GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE); 228 if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0) 229 id = IDR_PRODUCT_LOGO_WHITE; 230 231 base::RefCountedMemory* data = NULL; 232 if (theme_supplier_.get()) 233 data = theme_supplier_->GetRawData(id, scale_factor); 234 if (!data) 235 data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P); 236 237 return data; 238} 239 240void ThemeService::Observe(int type, 241 const content::NotificationSource& source, 242 const content::NotificationDetails& details) { 243 using content::Details; 244 switch (type) { 245 case chrome::NOTIFICATION_EXTENSIONS_READY: 246 registrar_.Remove(this, chrome::NOTIFICATION_EXTENSIONS_READY, 247 content::Source<Profile>(profile_)); 248 OnExtensionServiceReady(); 249 break; 250 case chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED: { 251 // The theme may be initially disabled. Wait till it is loaded (if ever). 252 Details<const extensions::InstalledExtensionInfo> installed_details( 253 details); 254 if (installed_details->extension->is_theme()) 255 installed_pending_load_id_ = installed_details->extension->id(); 256 break; 257 } 258 case chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED: 259 { 260 const Extension* extension = Details<const Extension>(details).ptr(); 261 if (extension->is_theme() && 262 installed_pending_load_id_ != kDefaultThemeID && 263 installed_pending_load_id_ == extension->id()) { 264 SetTheme(extension); 265 } 266 installed_pending_load_id_ = kDefaultThemeID; 267 break; 268 } 269 case chrome::NOTIFICATION_EXTENSION_ENABLED: 270 { 271 const Extension* extension = Details<const Extension>(details).ptr(); 272 if (extension->is_theme()) 273 SetTheme(extension); 274 break; 275 } 276 case chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED: 277 { 278 Details<const UnloadedExtensionInfo> unloaded_details(details); 279 if (unloaded_details->reason != UnloadedExtensionInfo::REASON_UPDATE && 280 unloaded_details->extension->is_theme() && 281 unloaded_details->extension->id() == GetThemeID()) { 282 UseDefaultTheme(); 283 } 284 break; 285 } 286 } 287} 288 289void ThemeService::SetTheme(const Extension* extension) { 290 DCHECK(extension->is_theme()); 291 ExtensionService* service = 292 extensions::ExtensionSystem::Get(profile_)->extension_service(); 293 if (!service->IsExtensionEnabled(extension->id())) { 294 // |extension| is disabled when reverting to the previous theme via an 295 // infobar. 296 service->EnableExtension(extension->id()); 297 // Enabling the extension will call back to SetTheme(). 298 return; 299 } 300 301 std::string previous_theme_id = GetThemeID(); 302 303 // Clear our image cache. 304 FreePlatformCaches(); 305 306 BuildFromExtension(extension); 307 SaveThemeID(extension->id()); 308 309 NotifyThemeChanged(); 310 content::RecordAction(UserMetricsAction("Themes_Installed")); 311 312 if (previous_theme_id != kDefaultThemeID && 313 previous_theme_id != extension->id()) { 314 // Disable the old theme. 315 service->DisableExtension(previous_theme_id, 316 extensions::Extension::DISABLE_USER_ACTION); 317 } 318} 319 320void ThemeService::SetCustomDefaultTheme( 321 scoped_refptr<CustomThemeSupplier> theme_supplier) { 322 ClearAllThemeData(); 323 SwapThemeSupplier(theme_supplier); 324 NotifyThemeChanged(); 325} 326 327bool ThemeService::ShouldInitWithSystemTheme() const { 328 return false; 329} 330 331void ThemeService::RemoveUnusedThemes(bool ignore_infobars) { 332 // We do not want to garbage collect themes on startup (|ready_| is false). 333 // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|. 334 if (!profile_ || !ready_) 335 return; 336 if (!ignore_infobars && number_of_infobars_ != 0) 337 return; 338 339 ExtensionService* service = 340 extensions::ExtensionSystem::Get(profile_)->extension_service(); 341 if (!service) 342 return; 343 344 std::string current_theme = GetThemeID(); 345 std::vector<std::string> remove_list; 346 scoped_ptr<const extensions::ExtensionSet> extensions( 347 extensions::ExtensionRegistry::Get(profile_) 348 ->GenerateInstalledExtensionsSet()); 349 extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_); 350 for (extensions::ExtensionSet::const_iterator it = extensions->begin(); 351 it != extensions->end(); ++it) { 352 const extensions::Extension* extension = *it; 353 if (extension->is_theme() && 354 extension->id() != current_theme) { 355 // Only uninstall themes which are not disabled or are disabled with 356 // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled 357 // themes because externally installed themes are initially disabled. 358 int disable_reason = prefs->GetDisableReasons(extension->id()); 359 if (!prefs->IsExtensionDisabled(extension->id()) || 360 disable_reason == Extension::DISABLE_USER_ACTION) { 361 remove_list.push_back((*it)->id()); 362 } 363 } 364 } 365 // TODO: Garbage collect all unused themes. This method misses themes which 366 // are installed but not loaded because they are blacklisted by a management 367 // policy provider. 368 369 for (size_t i = 0; i < remove_list.size(); ++i) 370 service->UninstallExtension(remove_list[i], false, NULL); 371} 372 373void ThemeService::UseDefaultTheme() { 374 if (ready_) 375 content::RecordAction(UserMetricsAction("Themes_Reset")); 376 if (IsManagedUser()) { 377 SetManagedUserTheme(); 378 return; 379 } 380 ClearAllThemeData(); 381 NotifyThemeChanged(); 382} 383 384void ThemeService::UseSystemTheme() { 385 UseDefaultTheme(); 386} 387 388bool ThemeService::UsingDefaultTheme() const { 389 std::string id = GetThemeID(); 390 return id == ThemeService::kDefaultThemeID || 391 id == kDefaultThemeGalleryID; 392} 393 394std::string ThemeService::GetThemeID() const { 395 return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID); 396} 397 398color_utils::HSL ThemeService::GetTint(int id) const { 399 DCHECK(CalledOnValidThread()); 400 401 color_utils::HSL hsl; 402 if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl)) 403 return hsl; 404 405 return ThemeProperties::GetDefaultTint(id); 406} 407 408void ThemeService::ClearAllThemeData() { 409 if (!ready_) 410 return; 411 412 SwapThemeSupplier(NULL); 413 414 // Clear our image cache. 415 FreePlatformCaches(); 416 417 profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename); 418 SaveThemeID(kDefaultThemeID); 419 420 // There should be no more infobars. This may not be the case because of 421 // http://crbug.com/62154 422 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may 423 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED. 424 base::MessageLoop::current()->PostTask(FROM_HERE, 425 base::Bind(&ThemeService::RemoveUnusedThemes, 426 weak_ptr_factory_.GetWeakPtr(), 427 true)); 428} 429 430void ThemeService::LoadThemePrefs() { 431 PrefService* prefs = profile_->GetPrefs(); 432 433 std::string current_id = GetThemeID(); 434 if (current_id == kDefaultThemeID) { 435 // Managed users have a different default theme. 436 if (IsManagedUser()) 437 SetManagedUserTheme(); 438 else if (ShouldInitWithSystemTheme()) 439 UseSystemTheme(); 440 else 441 UseDefaultTheme(); 442 set_ready(); 443 return; 444 } 445 446 bool loaded_pack = false; 447 448 // If we don't have a file pack, we're updating from an old version. 449 base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename); 450 if (path != base::FilePath()) { 451 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id)); 452 loaded_pack = theme_supplier_.get() != NULL; 453 } 454 455 if (loaded_pack) { 456 content::RecordAction(UserMetricsAction("Themes.Loaded")); 457 set_ready(); 458 } 459 // Else: wait for the extension service to be ready so that the theme pack 460 // can be recreated from the extension. 461} 462 463void ThemeService::NotifyThemeChanged() { 464 if (!ready_) 465 return; 466 467 DVLOG(1) << "Sending BROWSER_THEME_CHANGED"; 468 // Redraw! 469 content::NotificationService* service = 470 content::NotificationService::current(); 471 service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 472 content::Source<ThemeService>(this), 473 content::NotificationService::NoDetails()); 474#if defined(OS_MACOSX) 475 NotifyPlatformThemeChanged(); 476#endif // OS_MACOSX 477 478 // Notify sync that theme has changed. 479 if (theme_syncable_service_.get()) { 480 theme_syncable_service_->OnThemeChange(); 481 } 482} 483 484#if defined(USE_AURA) 485void ThemeService::FreePlatformCaches() { 486 // Views (Skia) has no platform image cache to clear. 487} 488#endif 489 490void ThemeService::OnExtensionServiceReady() { 491 if (!ready_) { 492 // If the ThemeService is not ready yet, the custom theme data pack needs to 493 // be recreated from the extension. 494 MigrateTheme(); 495 set_ready(); 496 497 // Send notification in case anyone requested data and cached it when the 498 // theme service was not ready yet. 499 NotifyThemeChanged(); 500 } 501 502 registrar_.Add(this, 503 chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED, 504 content::Source<Profile>(profile_)); 505 registrar_.Add(this, 506 chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED, 507 content::Source<Profile>(profile_)); 508 registrar_.Add(this, 509 chrome::NOTIFICATION_EXTENSION_ENABLED, 510 content::Source<Profile>(profile_)); 511 registrar_.Add(this, 512 chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED, 513 content::Source<Profile>(profile_)); 514 515 base::MessageLoop::current()->PostDelayedTask(FROM_HERE, 516 base::Bind(&ThemeService::RemoveUnusedThemes, 517 weak_ptr_factory_.GetWeakPtr(), 518 false), 519 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay)); 520} 521 522void ThemeService::MigrateTheme() { 523 // TODO(erg): We need to pop up a dialog informing the user that their 524 // theme is being migrated. 525 ExtensionService* service = 526 extensions::ExtensionSystem::Get(profile_)->extension_service(); 527 const Extension* extension = service ? 528 service->GetExtensionById(GetThemeID(), false) : NULL; 529 if (extension) { 530 DLOG(ERROR) << "Migrating theme"; 531 BuildFromExtension(extension); 532 content::RecordAction(UserMetricsAction("Themes.Migrated")); 533 } else { 534 DLOG(ERROR) << "Theme is mysteriously gone."; 535 ClearAllThemeData(); 536 content::RecordAction(UserMetricsAction("Themes.Gone")); 537 } 538} 539 540void ThemeService::SwapThemeSupplier( 541 scoped_refptr<CustomThemeSupplier> theme_supplier) { 542 if (theme_supplier_.get()) 543 theme_supplier_->StopUsingTheme(); 544 theme_supplier_ = theme_supplier; 545 if (theme_supplier_.get()) 546 theme_supplier_->StartUsingTheme(); 547} 548 549void ThemeService::SavePackName(const base::FilePath& pack_path) { 550 profile_->GetPrefs()->SetFilePath( 551 prefs::kCurrentThemePackFilename, pack_path); 552} 553 554void ThemeService::SaveThemeID(const std::string& id) { 555 profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id); 556} 557 558void ThemeService::BuildFromExtension(const Extension* extension) { 559 scoped_refptr<BrowserThemePack> pack( 560 BrowserThemePack::BuildFromExtension(extension)); 561 if (!pack.get()) { 562 // TODO(erg): We've failed to install the theme; perhaps we should tell the 563 // user? http://crbug.com/34780 564 LOG(ERROR) << "Could not load theme."; 565 return; 566 } 567 568 ExtensionService* service = 569 extensions::ExtensionSystem::Get(profile_)->extension_service(); 570 if (!service) 571 return; 572 573 // Write the packed file to disk. 574 base::FilePath pack_path = 575 extension->path().Append(chrome::kThemePackFilename); 576 service->GetFileTaskRunner()->PostTask( 577 FROM_HERE, 578 base::Bind(&WritePackToDiskCallback, pack, pack_path)); 579 580 SavePackName(pack_path); 581 SwapThemeSupplier(pack); 582} 583 584bool ThemeService::IsManagedUser() const { 585 return profile_->IsManaged(); 586} 587 588void ThemeService::SetManagedUserTheme() { 589 SetCustomDefaultTheme(new ManagedUserTheme); 590} 591 592void ThemeService::OnInfobarDisplayed() { 593 number_of_infobars_++; 594} 595 596void ThemeService::OnInfobarDestroyed() { 597 number_of_infobars_--; 598 599 if (number_of_infobars_ == 0) 600 RemoveUnusedThemes(false); 601} 602 603ThemeSyncableService* ThemeService::GetThemeSyncableService() const { 604 return theme_syncable_service_.get(); 605} 606