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/profiles/profile.h" 17 #include "chrome/browser/supervised_user/supervised_user_theme.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 41 using base::UserMetricsAction; 42 using content::BrowserThread; 43 using extensions::Extension; 44 using extensions::UnloadedExtensionInfo; 45 using ui::ResourceBundle; 46 47 typedef 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. 51 const char* ThemeService::kDefaultThemeID = ""; 52 53 namespace { 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.) 59 const 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. 65 const int kRemoveUnusedThemesStartupDelay = 30; 66 67 SkColor 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. 75 void 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 83 ThemeService::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 92 ThemeService::~ThemeService() { 93 FreePlatformCaches(); 94 } 95 96 void 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 109 gfx::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 122 bool ThemeService::UsingSystemTheme() const { 123 return UsingDefaultTheme(); 124 } 125 126 gfx::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 135 SkColor 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_SUPERVISED_USER_LABEL: 155 return color_utils::GetReadableColor( 156 SK_ColorWHITE, 157 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND)); 158 case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND: 159 return color_utils::BlendTowardOppositeLuminance( 160 GetColor(Properties::COLOR_FRAME), 0x80); 161 case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER: 162 return color_utils::AlphaBlend( 163 GetColor(Properties::COLOR_SUPERVISED_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 185 int 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 203 bool 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 213 bool 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 223 base::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 240 void 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 289 void 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 320 void ThemeService::SetCustomDefaultTheme( 321 scoped_refptr<CustomThemeSupplier> theme_supplier) { 322 ClearAllThemeData(); 323 SwapThemeSupplier(theme_supplier); 324 NotifyThemeChanged(); 325 } 326 327 bool ThemeService::ShouldInitWithSystemTheme() const { 328 return false; 329 } 330 331 void 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 373 void ThemeService::UseDefaultTheme() { 374 if (ready_) 375 content::RecordAction(UserMetricsAction("Themes_Reset")); 376 if (IsSupervisedUser()) { 377 SetSupervisedUserTheme(); 378 return; 379 } 380 ClearAllThemeData(); 381 NotifyThemeChanged(); 382 } 383 384 void ThemeService::UseSystemTheme() { 385 UseDefaultTheme(); 386 } 387 388 bool ThemeService::UsingDefaultTheme() const { 389 std::string id = GetThemeID(); 390 return id == ThemeService::kDefaultThemeID || 391 id == kDefaultThemeGalleryID; 392 } 393 394 std::string ThemeService::GetThemeID() const { 395 return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID); 396 } 397 398 color_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 408 void 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 430 void ThemeService::LoadThemePrefs() { 431 PrefService* prefs = profile_->GetPrefs(); 432 433 std::string current_id = GetThemeID(); 434 if (current_id == kDefaultThemeID) { 435 // Supervised users have a different default theme. 436 if (IsSupervisedUser()) 437 SetSupervisedUserTheme(); 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 463 void 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) 485 void ThemeService::FreePlatformCaches() { 486 // Views (Skia) has no platform image cache to clear. 487 } 488 #endif 489 490 void 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 522 void 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 540 void 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 549 void ThemeService::SavePackName(const base::FilePath& pack_path) { 550 profile_->GetPrefs()->SetFilePath( 551 prefs::kCurrentThemePackFilename, pack_path); 552 } 553 554 void ThemeService::SaveThemeID(const std::string& id) { 555 profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id); 556 } 557 558 void 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 584 bool ThemeService::IsSupervisedUser() const { 585 return profile_->IsSupervised(); 586 } 587 588 void ThemeService::SetSupervisedUserTheme() { 589 SetCustomDefaultTheme(new SupervisedUserTheme); 590 } 591 592 void ThemeService::OnInfobarDisplayed() { 593 number_of_infobars_++; 594 } 595 596 void ThemeService::OnInfobarDestroyed() { 597 number_of_infobars_--; 598 599 if (number_of_infobars_ == 0) 600 RemoveUnusedThemes(false); 601 } 602 603 ThemeSyncableService* ThemeService::GetThemeSyncableService() const { 604 return theme_syncable_service_.get(); 605 } 606