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