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.
5 #include "chrome/browser/themes/theme_service.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 "grit/ui_resources.h"
36 #include "ui/base/layout.h"
37 #include "ui/base/resource/resource_bundle.h"
38 #include "ui/gfx/image/image_skia.h"
41 #include "ui/base/win/shell.h"
44 using base::UserMetricsAction;
45 using content::BrowserThread;
46 using extensions::Extension;
47 using extensions::UnloadedExtensionInfo;
48 using ui::ResourceBundle;
50 typedef ThemeProperties Properties;
52 // The default theme if we haven't installed a theme yet or if we've clicked
53 // the "Use Classic" button.
54 const char* ThemeService::kDefaultThemeID = "";
58 // The default theme if we've gone to the theme gallery and installed the
59 // "Default" theme. We have to detect this case specifically. (By the time we
60 // realize we've installed the default theme, we already have an extension
61 // unpacked on the filesystem.)
62 const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn";
64 // Wait this many seconds after startup to garbage collect unused themes.
65 // Removing unused themes is done after a delay because there is no
66 // reason to do it at startup.
67 // ExtensionService::GarbageCollectExtensions() does something similar.
68 const int kRemoveUnusedThemesStartupDelay = 30;
70 SkColor IncreaseLightness(SkColor color, double percent) {
71 color_utils::HSL result;
72 color_utils::SkColorToHSL(color, &result);
73 result.l += (1 - result.l) * percent;
74 return color_utils::HSLToSkColor(result, SkColorGetA(color));
77 // Writes the theme pack to disk on a separate thread.
78 void WritePackToDiskCallback(BrowserThemePack* pack,
79 const base::FilePath& path) {
80 if (!pack->WriteToDisk(path))
81 NOTREACHED() << "Could not write theme pack to disk";
84 // Heuristic to determine if color is grayscale. This is used to decide whether
85 // to use the colorful or white logo, if a theme fails to specify which.
86 bool IsColorGrayscale(SkColor color) {
87 const int kChannelTolerance = 9;
88 int r = SkColorGetR(color);
89 int g = SkColorGetG(color);
90 int b = SkColorGetB(color);
91 int range = std::max(r, std::max(g, b)) - std::min(r, std::min(g, b));
92 return range < kChannelTolerance;
97 ThemeService::ThemeService()
99 rb_(ResourceBundle::GetSharedInstance()),
101 installed_pending_load_id_(kDefaultThemeID),
102 number_of_infobars_(0),
103 weak_ptr_factory_(this) {
106 ThemeService::~ThemeService() {
107 FreePlatformCaches();
110 void ThemeService::Init(Profile* profile) {
111 DCHECK(CalledOnValidThread());
117 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
118 content::Source<Profile>(profile_));
120 theme_syncable_service_.reset(new ThemeSyncableService(profile_, this));
123 gfx::Image ThemeService::GetImageNamed(int id) const {
124 DCHECK(CalledOnValidThread());
127 if (theme_supplier_.get())
128 image = theme_supplier_->GetImageNamed(id);
131 image = rb_.GetNativeImageNamed(id);
136 bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
140 bool ThemeService::UsingSystemTheme() const {
141 return UsingDefaultTheme();
144 gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const {
145 gfx::Image image = GetImageNamed(id);
148 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns
149 // its images const. GetImageSkiaNamed() also should but has many callsites.
150 return const_cast<gfx::ImageSkia*>(image.ToImageSkia());
153 SkColor ThemeService::GetColor(int id) const {
154 DCHECK(CalledOnValidThread());
156 if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color))
159 // For backward compat with older themes, some newer colors are generated from
160 // older ones if they are missing.
162 case Properties::COLOR_NTP_SECTION_HEADER_TEXT:
163 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30);
164 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER:
165 return GetColor(Properties::COLOR_NTP_TEXT);
166 case Properties::COLOR_NTP_SECTION_HEADER_RULE:
167 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70);
168 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT:
169 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86);
170 case Properties::COLOR_NTP_TEXT_LIGHT:
171 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40);
172 case Properties::COLOR_SUPERVISED_USER_LABEL:
173 return color_utils::GetReadableColor(
175 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND));
176 case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND:
177 return color_utils::BlendTowardOppositeLuminance(
178 GetColor(Properties::COLOR_FRAME), 0x80);
179 case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER:
180 return color_utils::AlphaBlend(
181 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND),
184 case Properties::COLOR_STATUS_BAR_TEXT: {
185 // A long time ago, we blended the toolbar and the tab text together to
186 // get the status bar text because, at the time, our text rendering in
187 // views couldn't do alpha blending. Even though this is no longer the
188 // case, this blending decision is built into the majority of themes that
189 // exist, and we must keep doing it.
190 SkColor toolbar_color = GetColor(Properties::COLOR_TOOLBAR);
191 SkColor text_color = GetColor(Properties::COLOR_TAB_TEXT);
192 return SkColorSetARGB(
193 SkColorGetA(text_color),
194 (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2,
195 (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2,
196 (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2);
200 return Properties::GetDefaultColor(id);
203 int ThemeService::GetDisplayProperty(int id) const {
205 if (theme_supplier_.get() &&
206 theme_supplier_->GetDisplayProperty(id, &result)) {
210 if (id == Properties::NTP_LOGO_ALTERNATE) {
211 if (UsingDefaultTheme() || UsingSystemTheme())
212 return 0; // Colorful logo.
214 if (HasCustomImage(IDR_THEME_NTP_BACKGROUND))
215 return 1; // White logo.
217 SkColor background_color = GetColor(Properties::COLOR_NTP_BACKGROUND);
218 return IsColorGrayscale(background_color) ? 0 : 1;
221 return Properties::GetDefaultDisplayProperty(id);
224 bool ThemeService::ShouldUseNativeFrame() const {
225 if (HasCustomImage(IDR_THEME_FRAME))
228 return ui::win::IsAeroGlassEnabled();
234 bool ThemeService::HasCustomImage(int id) const {
235 if (!Properties::IsThemeableImage(id))
238 if (theme_supplier_.get())
239 return theme_supplier_->HasCustomImage(id);
244 base::RefCountedMemory* ThemeService::GetRawData(
246 ui::ScaleFactor scale_factor) const {
247 // Check to see whether we should substitute some images.
248 int ntp_alternate = GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE);
249 if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0)
250 id = IDR_PRODUCT_LOGO_WHITE;
252 base::RefCountedMemory* data = NULL;
253 if (theme_supplier_.get())
254 data = theme_supplier_->GetRawData(id, scale_factor);
256 data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P);
261 void ThemeService::Observe(int type,
262 const content::NotificationSource& source,
263 const content::NotificationDetails& details) {
264 using content::Details;
266 case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED:
267 registrar_.Remove(this,
268 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
269 content::Source<Profile>(profile_));
270 OnExtensionServiceReady();
272 case extensions::NOTIFICATION_EXTENSION_WILL_BE_INSTALLED_DEPRECATED: {
273 // The theme may be initially disabled. Wait till it is loaded (if ever).
274 Details<const extensions::InstalledExtensionInfo> installed_details(
276 if (installed_details->extension->is_theme())
277 installed_pending_load_id_ = installed_details->extension->id();
280 case extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED: {
281 const Extension* extension = Details<const Extension>(details).ptr();
282 if (extension->is_theme() &&
283 installed_pending_load_id_ != kDefaultThemeID &&
284 installed_pending_load_id_ == extension->id()) {
287 installed_pending_load_id_ = kDefaultThemeID;
290 case extensions::NOTIFICATION_EXTENSION_ENABLED: {
291 const Extension* extension = Details<const Extension>(details).ptr();
292 if (extension->is_theme())
296 case extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED: {
297 Details<const UnloadedExtensionInfo> unloaded_details(details);
298 if (unloaded_details->reason != UnloadedExtensionInfo::REASON_UPDATE &&
299 unloaded_details->extension->is_theme() &&
300 unloaded_details->extension->id() == GetThemeID()) {
308 void ThemeService::SetTheme(const Extension* extension) {
309 DCHECK(extension->is_theme());
310 ExtensionService* service =
311 extensions::ExtensionSystem::Get(profile_)->extension_service();
312 if (!service->IsExtensionEnabled(extension->id())) {
313 // |extension| is disabled when reverting to the previous theme via an
315 service->EnableExtension(extension->id());
316 // Enabling the extension will call back to SetTheme().
320 std::string previous_theme_id = GetThemeID();
322 // Clear our image cache.
323 FreePlatformCaches();
325 BuildFromExtension(extension);
326 SaveThemeID(extension->id());
328 NotifyThemeChanged();
329 content::RecordAction(UserMetricsAction("Themes_Installed"));
331 if (previous_theme_id != kDefaultThemeID &&
332 previous_theme_id != extension->id()) {
333 // Disable the old theme.
334 service->DisableExtension(previous_theme_id,
335 extensions::Extension::DISABLE_USER_ACTION);
339 void ThemeService::SetCustomDefaultTheme(
340 scoped_refptr<CustomThemeSupplier> theme_supplier) {
342 SwapThemeSupplier(theme_supplier);
343 NotifyThemeChanged();
346 bool ThemeService::ShouldInitWithSystemTheme() const {
350 void ThemeService::RemoveUnusedThemes(bool ignore_infobars) {
351 // We do not want to garbage collect themes on startup (|ready_| is false).
352 // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
353 if (!profile_ || !ready_)
355 if (!ignore_infobars && number_of_infobars_ != 0)
358 ExtensionService* service =
359 extensions::ExtensionSystem::Get(profile_)->extension_service();
363 std::string current_theme = GetThemeID();
364 std::vector<std::string> remove_list;
365 scoped_ptr<const extensions::ExtensionSet> extensions(
366 extensions::ExtensionRegistry::Get(profile_)
367 ->GenerateInstalledExtensionsSet());
368 extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
369 for (extensions::ExtensionSet::const_iterator it = extensions->begin();
370 it != extensions->end(); ++it) {
371 const extensions::Extension* extension = *it;
372 if (extension->is_theme() &&
373 extension->id() != current_theme) {
374 // Only uninstall themes which are not disabled or are disabled with
375 // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
376 // themes because externally installed themes are initially disabled.
377 int disable_reason = prefs->GetDisableReasons(extension->id());
378 if (!prefs->IsExtensionDisabled(extension->id()) ||
379 disable_reason == Extension::DISABLE_USER_ACTION) {
380 remove_list.push_back((*it)->id());
384 // TODO: Garbage collect all unused themes. This method misses themes which
385 // are installed but not loaded because they are blacklisted by a management
388 for (size_t i = 0; i < remove_list.size(); ++i) {
389 service->UninstallExtension(remove_list[i],
390 extensions::UNINSTALL_REASON_ORPHANED_THEME,
391 base::Bind(&base::DoNothing),
396 void ThemeService::UseDefaultTheme() {
398 content::RecordAction(UserMetricsAction("Themes_Reset"));
399 if (IsSupervisedUser()) {
400 SetSupervisedUserTheme();
404 NotifyThemeChanged();
407 void ThemeService::UseSystemTheme() {
411 bool ThemeService::UsingDefaultTheme() const {
412 std::string id = GetThemeID();
413 return id == ThemeService::kDefaultThemeID ||
414 id == kDefaultThemeGalleryID;
417 std::string ThemeService::GetThemeID() const {
418 return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
421 color_utils::HSL ThemeService::GetTint(int id) const {
422 DCHECK(CalledOnValidThread());
424 color_utils::HSL hsl;
425 if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl))
428 return ThemeProperties::GetDefaultTint(id);
431 void ThemeService::ClearAllThemeData() {
435 SwapThemeSupplier(NULL);
437 // Clear our image cache.
438 FreePlatformCaches();
440 profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
441 SaveThemeID(kDefaultThemeID);
443 // There should be no more infobars. This may not be the case because of
444 // http://crbug.com/62154
445 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
446 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
447 base::MessageLoop::current()->PostTask(FROM_HERE,
448 base::Bind(&ThemeService::RemoveUnusedThemes,
449 weak_ptr_factory_.GetWeakPtr(),
453 void ThemeService::LoadThemePrefs() {
454 PrefService* prefs = profile_->GetPrefs();
456 std::string current_id = GetThemeID();
457 if (current_id == kDefaultThemeID) {
458 // Supervised users have a different default theme.
459 if (IsSupervisedUser())
460 SetSupervisedUserTheme();
461 else if (ShouldInitWithSystemTheme())
469 bool loaded_pack = false;
471 // If we don't have a file pack, we're updating from an old version.
472 base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
473 if (path != base::FilePath()) {
474 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id));
475 loaded_pack = theme_supplier_.get() != NULL;
479 content::RecordAction(UserMetricsAction("Themes.Loaded"));
482 // Else: wait for the extension service to be ready so that the theme pack
483 // can be recreated from the extension.
486 void ThemeService::NotifyThemeChanged() {
490 DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
492 content::NotificationService* service =
493 content::NotificationService::current();
494 service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
495 content::Source<ThemeService>(this),
496 content::NotificationService::NoDetails());
497 #if defined(OS_MACOSX)
498 NotifyPlatformThemeChanged();
501 // Notify sync that theme has changed.
502 if (theme_syncable_service_.get()) {
503 theme_syncable_service_->OnThemeChange();
507 #if defined(USE_AURA)
508 void ThemeService::FreePlatformCaches() {
509 // Views (Skia) has no platform image cache to clear.
513 void ThemeService::OnExtensionServiceReady() {
515 // If the ThemeService is not ready yet, the custom theme data pack needs to
516 // be recreated from the extension.
520 // Send notification in case anyone requested data and cached it when the
521 // theme service was not ready yet.
522 NotifyThemeChanged();
527 extensions::NOTIFICATION_EXTENSION_WILL_BE_INSTALLED_DEPRECATED,
528 content::Source<Profile>(profile_));
530 extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
531 content::Source<Profile>(profile_));
533 extensions::NOTIFICATION_EXTENSION_ENABLED,
534 content::Source<Profile>(profile_));
536 extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED,
537 content::Source<Profile>(profile_));
539 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
540 base::Bind(&ThemeService::RemoveUnusedThemes,
541 weak_ptr_factory_.GetWeakPtr(),
543 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay));
546 void ThemeService::MigrateTheme() {
547 // TODO(erg): We need to pop up a dialog informing the user that their
548 // theme is being migrated.
549 ExtensionService* service =
550 extensions::ExtensionSystem::Get(profile_)->extension_service();
551 const Extension* extension = service ?
552 service->GetExtensionById(GetThemeID(), false) : NULL;
554 DLOG(ERROR) << "Migrating theme";
555 BuildFromExtension(extension);
556 content::RecordAction(UserMetricsAction("Themes.Migrated"));
558 DLOG(ERROR) << "Theme is mysteriously gone.";
560 content::RecordAction(UserMetricsAction("Themes.Gone"));
564 void ThemeService::SwapThemeSupplier(
565 scoped_refptr<CustomThemeSupplier> theme_supplier) {
566 if (theme_supplier_.get())
567 theme_supplier_->StopUsingTheme();
568 theme_supplier_ = theme_supplier;
569 if (theme_supplier_.get())
570 theme_supplier_->StartUsingTheme();
573 void ThemeService::SavePackName(const base::FilePath& pack_path) {
574 profile_->GetPrefs()->SetFilePath(
575 prefs::kCurrentThemePackFilename, pack_path);
578 void ThemeService::SaveThemeID(const std::string& id) {
579 profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
582 void ThemeService::BuildFromExtension(const Extension* extension) {
583 scoped_refptr<BrowserThemePack> pack(
584 BrowserThemePack::BuildFromExtension(extension));
586 // TODO(erg): We've failed to install the theme; perhaps we should tell the
587 // user? http://crbug.com/34780
588 LOG(ERROR) << "Could not load theme.";
592 ExtensionService* service =
593 extensions::ExtensionSystem::Get(profile_)->extension_service();
597 // Write the packed file to disk.
598 base::FilePath pack_path =
599 extension->path().Append(chrome::kThemePackFilename);
600 service->GetFileTaskRunner()->PostTask(
602 base::Bind(&WritePackToDiskCallback, pack, pack_path));
604 SavePackName(pack_path);
605 SwapThemeSupplier(pack);
608 bool ThemeService::IsSupervisedUser() const {
609 return profile_->IsSupervised();
612 void ThemeService::SetSupervisedUserTheme() {
613 SetCustomDefaultTheme(new SupervisedUserTheme);
616 void ThemeService::OnInfobarDisplayed() {
617 number_of_infobars_++;
620 void ThemeService::OnInfobarDestroyed() {
621 number_of_infobars_--;
623 if (number_of_infobars_ == 0)
624 RemoveUnusedThemes(false);
627 ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
628 return theme_syncable_service_.get();