// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/themes/theme_service.h" #include "base/bind.h" #include "base/memory/ref_counted_memory.h" #include "base/message_loop/message_loop.h" #include "base/prefs/pref_service.h" #include "base/sequenced_task_runner.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/managed_mode/managed_user_theme.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/themes/browser_theme_pack.h" #include "chrome/browser/themes/custom_theme_supplier.h" #include "chrome/browser/themes/theme_properties.h" #include "chrome/browser/themes/theme_syncable_service.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/pref_names.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/user_metrics.h" #include "grit/theme_resources.h" #include "grit/ui_resources.h" #include "ui/base/layout.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image_skia.h" #if defined(OS_WIN) #include "ui/base/win/shell.h" #endif using content::BrowserThread; using content::UserMetricsAction; using extensions::Extension; using extensions::UnloadedExtensionInfo; using ui::ResourceBundle; typedef ThemeProperties Properties; // The default theme if we haven't installed a theme yet or if we've clicked // the "Use Classic" button. const char* ThemeService::kDefaultThemeID = ""; namespace { // The default theme if we've gone to the theme gallery and installed the // "Default" theme. We have to detect this case specifically. (By the time we // realize we've installed the default theme, we already have an extension // unpacked on the filesystem.) const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn"; // Wait this many seconds after startup to garbage collect unused themes. // Removing unused themes is done after a delay because there is no // reason to do it at startup. // ExtensionService::GarbageCollectExtensions() does something similar. const int kRemoveUnusedThemesStartupDelay = 30; SkColor IncreaseLightness(SkColor color, double percent) { color_utils::HSL result; color_utils::SkColorToHSL(color, &result); result.l += (1 - result.l) * percent; return color_utils::HSLToSkColor(result, SkColorGetA(color)); } // Writes the theme pack to disk on a separate thread. void WritePackToDiskCallback(BrowserThemePack* pack, const base::FilePath& path) { if (!pack->WriteToDisk(path)) NOTREACHED() << "Could not write theme pack to disk"; } } // namespace ThemeService::ThemeService() : ready_(false), rb_(ResourceBundle::GetSharedInstance()), profile_(NULL), installed_pending_load_id_(kDefaultThemeID), number_of_infobars_(0), weak_ptr_factory_(this) { } ThemeService::~ThemeService() { FreePlatformCaches(); } void ThemeService::Init(Profile* profile) { DCHECK(CalledOnValidThread()); profile_ = profile; LoadThemePrefs(); registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY, content::Source<Profile>(profile_)); theme_syncable_service_.reset(new ThemeSyncableService(profile_, this)); } gfx::Image ThemeService::GetImageNamed(int id) const { DCHECK(CalledOnValidThread()); gfx::Image image; if (theme_supplier_.get()) image = theme_supplier_->GetImageNamed(id); if (image.IsEmpty()) image = rb_.GetNativeImageNamed(id); return image; } gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const { gfx::Image image = GetImageNamed(id); if (image.IsEmpty()) return NULL; // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns // its images const. GetImageSkiaNamed() also should but has many callsites. return const_cast<gfx::ImageSkia*>(image.ToImageSkia()); } SkColor ThemeService::GetColor(int id) const { DCHECK(CalledOnValidThread()); SkColor color; if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color)) return color; // For backward compat with older themes, some newer colors are generated from // older ones if they are missing. switch (id) { case Properties::COLOR_NTP_SECTION_HEADER_TEXT: return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30); case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER: return GetColor(Properties::COLOR_NTP_TEXT); case Properties::COLOR_NTP_SECTION_HEADER_RULE: return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70); case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT: return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86); case Properties::COLOR_NTP_TEXT_LIGHT: return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40); case Properties::COLOR_MANAGED_USER_LABEL: return color_utils::GetReadableColor( SK_ColorWHITE, GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND)); case Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND: return color_utils::BlendTowardOppositeLuminance( GetColor(Properties::COLOR_FRAME), 0x80); case Properties::COLOR_MANAGED_USER_LABEL_BORDER: return color_utils::AlphaBlend( GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND), SK_ColorBLACK, 230); case Properties::COLOR_STATUS_BAR_TEXT: { // A long time ago, we blended the toolbar and the tab text together to // get the status bar text because, at the time, our text rendering in // views couldn't do alpha blending. Even though this is no longer the // case, this blending decision is built into the majority of themes that // exist, and we must keep doing it. SkColor toolbar_color = GetColor(Properties::COLOR_TOOLBAR); SkColor text_color = GetColor(Properties::COLOR_TAB_TEXT); return SkColorSetARGB( SkColorGetA(text_color), (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2, (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2, (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2); } } return Properties::GetDefaultColor(id); } int ThemeService::GetDisplayProperty(int id) const { int result = 0; if (theme_supplier_.get() && theme_supplier_->GetDisplayProperty(id, &result)) { return result; } if (id == Properties::NTP_LOGO_ALTERNATE && !UsingDefaultTheme() && !UsingNativeTheme()) { // Use the alternate logo for themes from the web store except for // |kDefaultThemeGalleryID|. return 1; } return Properties::GetDefaultDisplayProperty(id); } bool ThemeService::ShouldUseNativeFrame() const { if (HasCustomImage(IDR_THEME_FRAME)) return false; #if defined(OS_WIN) return ui::win::IsAeroGlassEnabled(); #else return false; #endif } bool ThemeService::HasCustomImage(int id) const { if (!Properties::IsThemeableImage(id)) return false; if (theme_supplier_.get()) return theme_supplier_->HasCustomImage(id); return false; } base::RefCountedMemory* ThemeService::GetRawData( int id, ui::ScaleFactor scale_factor) const { // Check to see whether we should substitute some images. int ntp_alternate = GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE); if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0) id = IDR_PRODUCT_LOGO_WHITE; base::RefCountedMemory* data = NULL; if (theme_supplier_.get()) data = theme_supplier_->GetRawData(id, scale_factor); if (!data) data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P); return data; } void ThemeService::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { using content::Details; switch (type) { case chrome::NOTIFICATION_EXTENSIONS_READY: registrar_.Remove(this, chrome::NOTIFICATION_EXTENSIONS_READY, content::Source<Profile>(profile_)); OnExtensionServiceReady(); break; case chrome::NOTIFICATION_EXTENSION_INSTALLED: { // The theme may be initially disabled. Wait till it is loaded (if ever). Details<const extensions::InstalledExtensionInfo> installed_details( details); if (installed_details->extension->is_theme()) installed_pending_load_id_ = installed_details->extension->id(); break; } case chrome::NOTIFICATION_EXTENSION_LOADED: { const Extension* extension = Details<const Extension>(details).ptr(); if (extension->is_theme() && installed_pending_load_id_ != kDefaultThemeID && installed_pending_load_id_ == extension->id()) { SetTheme(extension); } installed_pending_load_id_ = kDefaultThemeID; break; } case chrome::NOTIFICATION_EXTENSION_ENABLED: { const Extension* extension = Details<const Extension>(details).ptr(); if (extension->is_theme()) SetTheme(extension); break; } case chrome::NOTIFICATION_EXTENSION_UNLOADED: { Details<const UnloadedExtensionInfo> unloaded_details(details); if (unloaded_details->reason != UnloadedExtensionInfo::REASON_UPDATE && unloaded_details->extension->is_theme() && unloaded_details->extension->id() == GetThemeID()) { UseDefaultTheme(); } break; } } } void ThemeService::SetTheme(const Extension* extension) { DCHECK(extension->is_theme()); ExtensionService* service = extensions::ExtensionSystem::Get(profile_)->extension_service(); if (!service->IsExtensionEnabled(extension->id())) { // |extension| is disabled when reverting to the previous theme via an // infobar. service->EnableExtension(extension->id()); // Enabling the extension will call back to SetTheme(). return; } std::string previous_theme_id = GetThemeID(); // Clear our image cache. FreePlatformCaches(); BuildFromExtension(extension); SaveThemeID(extension->id()); NotifyThemeChanged(); content::RecordAction(UserMetricsAction("Themes_Installed")); if (previous_theme_id != kDefaultThemeID && previous_theme_id != extension->id()) { // Disable the old theme. service->DisableExtension(previous_theme_id, extensions::Extension::DISABLE_USER_ACTION); } } void ThemeService::SetCustomDefaultTheme( scoped_refptr<CustomThemeSupplier> theme_supplier) { ClearAllThemeData(); SwapThemeSupplier(theme_supplier); NotifyThemeChanged(); } bool ThemeService::ShouldInitWithNativeTheme() const { return false; } void ThemeService::RemoveUnusedThemes(bool ignore_infobars) { // We do not want to garbage collect themes on startup (|ready_| is false). // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|. if (!profile_ || !ready_) return; if (!ignore_infobars && number_of_infobars_ != 0) return; ExtensionService* service = profile_->GetExtensionService(); if (!service) return; std::string current_theme = GetThemeID(); std::vector<std::string> remove_list; scoped_ptr<const ExtensionSet> extensions( service->GenerateInstalledExtensionsSet()); extensions::ExtensionPrefs* prefs = service->extension_prefs(); for (ExtensionSet::const_iterator it = extensions->begin(); it != extensions->end(); ++it) { const extensions::Extension* extension = *it; if (extension->is_theme() && extension->id() != current_theme) { // Only uninstall themes which are not disabled or are disabled with // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled // themes because externally installed themes are initially disabled. int disable_reason = prefs->GetDisableReasons(extension->id()); if (!prefs->IsExtensionDisabled(extension->id()) || disable_reason == Extension::DISABLE_USER_ACTION) { remove_list.push_back((*it)->id()); } } } // TODO: Garbage collect all unused themes. This method misses themes which // are installed but not loaded because they are blacklisted by a management // policy provider. for (size_t i = 0; i < remove_list.size(); ++i) service->UninstallExtension(remove_list[i], false, NULL); } void ThemeService::UseDefaultTheme() { if (ready_) content::RecordAction(UserMetricsAction("Themes_Reset")); if (IsManagedUser()) { SetManagedUserTheme(); return; } ClearAllThemeData(); NotifyThemeChanged(); } void ThemeService::SetNativeTheme() { UseDefaultTheme(); } bool ThemeService::UsingDefaultTheme() const { std::string id = GetThemeID(); return id == ThemeService::kDefaultThemeID || id == kDefaultThemeGalleryID; } bool ThemeService::UsingNativeTheme() const { return UsingDefaultTheme(); } std::string ThemeService::GetThemeID() const { return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID); } color_utils::HSL ThemeService::GetTint(int id) const { DCHECK(CalledOnValidThread()); color_utils::HSL hsl; if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl)) return hsl; return ThemeProperties::GetDefaultTint(id); } void ThemeService::ClearAllThemeData() { if (!ready_) return; SwapThemeSupplier(NULL); // Clear our image cache. FreePlatformCaches(); profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename); SaveThemeID(kDefaultThemeID); // There should be no more infobars. This may not be the case because of // http://crbug.com/62154 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may // be called as a result of NOTIFICATION_EXTENSION_UNLOADED. base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(&ThemeService::RemoveUnusedThemes, weak_ptr_factory_.GetWeakPtr(), true)); } void ThemeService::LoadThemePrefs() { PrefService* prefs = profile_->GetPrefs(); std::string current_id = GetThemeID(); if (current_id == kDefaultThemeID) { // Managed users have a different default theme. if (IsManagedUser()) SetManagedUserTheme(); else if (ShouldInitWithNativeTheme()) SetNativeTheme(); else UseDefaultTheme(); set_ready(); return; } bool loaded_pack = false; // If we don't have a file pack, we're updating from an old version. base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename); if (path != base::FilePath()) { SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id)); loaded_pack = theme_supplier_.get() != NULL; } if (loaded_pack) { content::RecordAction(UserMetricsAction("Themes.Loaded")); set_ready(); } // Else: wait for the extension service to be ready so that the theme pack // can be recreated from the extension. } void ThemeService::NotifyThemeChanged() { if (!ready_) return; DVLOG(1) << "Sending BROWSER_THEME_CHANGED"; // Redraw! content::NotificationService* service = content::NotificationService::current(); service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED, content::Source<ThemeService>(this), content::NotificationService::NoDetails()); #if defined(OS_MACOSX) NotifyPlatformThemeChanged(); #endif // OS_MACOSX // Notify sync that theme has changed. if (theme_syncable_service_.get()) { theme_syncable_service_->OnThemeChange(); } } #if defined(OS_WIN) || defined(USE_AURA) void ThemeService::FreePlatformCaches() { // Views (Skia) has no platform image cache to clear. } #endif void ThemeService::OnExtensionServiceReady() { if (!ready_) { // If the ThemeService is not ready yet, the custom theme data pack needs to // be recreated from the extension. MigrateTheme(); set_ready(); // Send notification in case anyone requested data and cached it when the // theme service was not ready yet. NotifyThemeChanged(); } registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALLED, content::Source<Profile>(profile_)); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, content::Source<Profile>(profile_)); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_ENABLED, content::Source<Profile>(profile_)); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, content::Source<Profile>(profile_)); base::MessageLoop::current()->PostDelayedTask(FROM_HERE, base::Bind(&ThemeService::RemoveUnusedThemes, weak_ptr_factory_.GetWeakPtr(), false), base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay)); } void ThemeService::MigrateTheme() { // TODO(erg): We need to pop up a dialog informing the user that their // theme is being migrated. ExtensionService* service = extensions::ExtensionSystem::Get(profile_)->extension_service(); const Extension* extension = service ? service->GetExtensionById(GetThemeID(), false) : NULL; if (extension) { DLOG(ERROR) << "Migrating theme"; BuildFromExtension(extension); content::RecordAction(UserMetricsAction("Themes.Migrated")); } else { DLOG(ERROR) << "Theme is mysteriously gone."; ClearAllThemeData(); content::RecordAction(UserMetricsAction("Themes.Gone")); } } void ThemeService::SwapThemeSupplier( scoped_refptr<CustomThemeSupplier> theme_supplier) { if (theme_supplier_.get()) theme_supplier_->StopUsingTheme(); theme_supplier_ = theme_supplier; if (theme_supplier_.get()) theme_supplier_->StartUsingTheme(); } void ThemeService::SavePackName(const base::FilePath& pack_path) { profile_->GetPrefs()->SetFilePath( prefs::kCurrentThemePackFilename, pack_path); } void ThemeService::SaveThemeID(const std::string& id) { profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id); } void ThemeService::BuildFromExtension(const Extension* extension) { scoped_refptr<BrowserThemePack> pack( BrowserThemePack::BuildFromExtension(extension)); if (!pack.get()) { // TODO(erg): We've failed to install the theme; perhaps we should tell the // user? http://crbug.com/34780 LOG(ERROR) << "Could not load theme."; return; } ExtensionService* service = extensions::ExtensionSystem::Get(profile_)->extension_service(); if (!service) return; // Write the packed file to disk. base::FilePath pack_path = extension->path().Append(chrome::kThemePackFilename); service->GetFileTaskRunner()->PostTask( FROM_HERE, base::Bind(&WritePackToDiskCallback, pack, pack_path)); SavePackName(pack_path); SwapThemeSupplier(pack); } bool ThemeService::IsManagedUser() const { return profile_->IsManaged(); } void ThemeService::SetManagedUserTheme() { SetCustomDefaultTheme(new ManagedUserTheme); } void ThemeService::OnInfobarDisplayed() { number_of_infobars_++; } void ThemeService::OnInfobarDestroyed() { number_of_infobars_--; if (number_of_infobars_ == 0) RemoveUnusedThemes(false); } ThemeSyncableService* ThemeService::GetThemeSyncableService() const { return theme_syncable_service_.get(); }