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