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/location.h"
11 #include "base/memory/ref_counted_memory.h"
12 #include "base/prefs/pref_service.h"
13 #include "base/sequenced_task_runner.h"
14 #include "base/single_thread_task_runner.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/thread_task_runner_handle.h"
18 #include "chrome/browser/chrome_notification_types.h"
19 #include "chrome/browser/extensions/extension_service.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/themes/browser_theme_pack.h"
22 #include "chrome/browser/themes/custom_theme_supplier.h"
23 #include "chrome/browser/themes/theme_properties.h"
24 #include "chrome/browser/themes/theme_syncable_service.h"
25 #include "chrome/common/chrome_constants.h"
26 #include "chrome/common/pref_names.h"
27 #include "content/public/browser/notification_service.h"
28 #include "content/public/browser/user_metrics.h"
29 #include "extensions/browser/extension_prefs.h"
30 #include "extensions/browser/extension_registry.h"
31 #include "extensions/browser/extension_system.h"
32 #include "extensions/browser/uninstall_reason.h"
33 #include "extensions/common/extension.h"
34 #include "extensions/common/extension_set.h"
35 #include "grit/theme_resources.h"
36 #include "ui/base/layout.h"
37 #include "ui/base/resource/resource_bundle.h"
38 #include "ui/gfx/image/image_skia.h"
39 #include "ui/native_theme/common_theme.h"
40 #include "ui/native_theme/native_theme.h"
42 #if defined(ENABLE_EXTENSIONS)
43 #include "extensions/browser/extension_registry_observer.h"
46 #if defined(ENABLE_SUPERVISED_USERS)
47 #include "chrome/browser/supervised_user/supervised_user_theme.h"
51 #include "ui/base/win/shell.h"
54 using base::UserMetricsAction
;
55 using content::BrowserThread
;
56 using extensions::Extension
;
57 using extensions::UnloadedExtensionInfo
;
58 using ui::ResourceBundle
;
60 typedef ThemeProperties Properties
;
62 // The default theme if we haven't installed a theme yet or if we've clicked
63 // the "Use Classic" button.
64 const char* ThemeService::kDefaultThemeID
= "";
68 // The default theme if we've gone to the theme gallery and installed the
69 // "Default" theme. We have to detect this case specifically. (By the time we
70 // realize we've installed the default theme, we already have an extension
71 // unpacked on the filesystem.)
72 const char* kDefaultThemeGalleryID
= "hkacjpbfdknhflllbcmjibkdeoafencn";
74 // Wait this many seconds after startup to garbage collect unused themes.
75 // Removing unused themes is done after a delay because there is no
76 // reason to do it at startup.
77 // ExtensionService::GarbageCollectExtensions() does something similar.
78 const int kRemoveUnusedThemesStartupDelay
= 30;
80 SkColor
IncreaseLightness(SkColor color
, double percent
) {
81 color_utils::HSL result
;
82 color_utils::SkColorToHSL(color
, &result
);
83 result
.l
+= (1 - result
.l
) * percent
;
84 return color_utils::HSLToSkColor(result
, SkColorGetA(color
));
87 // Writes the theme pack to disk on a separate thread.
88 void WritePackToDiskCallback(BrowserThemePack
* pack
,
89 const base::FilePath
& path
) {
90 if (!pack
->WriteToDisk(path
))
91 NOTREACHED() << "Could not write theme pack to disk";
94 // Heuristic to determine if color is grayscale. This is used to decide whether
95 // to use the colorful or white logo, if a theme fails to specify which.
96 bool IsColorGrayscale(SkColor color
) {
97 const int kChannelTolerance
= 9;
98 int r
= SkColorGetR(color
);
99 int g
= SkColorGetG(color
);
100 int b
= SkColorGetB(color
);
101 int range
= std::max(r
, std::max(g
, b
)) - std::min(r
, std::min(g
, b
));
102 return range
< kChannelTolerance
;
107 #if defined(ENABLE_EXTENSIONS)
108 class ThemeService::ThemeObserver
109 : public extensions::ExtensionRegistryObserver
{
111 explicit ThemeObserver(ThemeService
* service
) : theme_service_(service
) {
112 extensions::ExtensionRegistry::Get(theme_service_
->profile_
)
116 ~ThemeObserver() override
{
117 extensions::ExtensionRegistry::Get(theme_service_
->profile_
)
118 ->RemoveObserver(this);
122 void OnExtensionWillBeInstalled(content::BrowserContext
* browser_context
,
123 const extensions::Extension
* extension
,
126 const std::string
& old_name
) override
{
127 if (extension
->is_theme()) {
128 // The theme may be initially disabled. Wait till it is loaded (if ever).
129 theme_service_
->installed_pending_load_id_
= extension
->id();
133 void OnExtensionLoaded(content::BrowserContext
* browser_context
,
134 const extensions::Extension
* extension
) override
{
135 if (extension
->is_theme() &&
136 theme_service_
->installed_pending_load_id_
!= kDefaultThemeID
&&
137 theme_service_
->installed_pending_load_id_
== extension
->id()) {
138 theme_service_
->SetTheme(extension
);
140 theme_service_
->installed_pending_load_id_
= kDefaultThemeID
;
143 void OnExtensionUnloaded(
144 content::BrowserContext
* browser_context
,
145 const extensions::Extension
* extension
,
146 extensions::UnloadedExtensionInfo::Reason reason
) override
{
147 if (reason
!= extensions::UnloadedExtensionInfo::REASON_UPDATE
&&
148 reason
!= extensions::UnloadedExtensionInfo::REASON_LOCK_ALL
&&
149 extension
->is_theme() &&
150 extension
->id() == theme_service_
->GetThemeID()) {
151 theme_service_
->UseDefaultTheme();
155 ThemeService
* theme_service_
;
157 #endif // defined(ENABLE_EXTENSIONS)
159 ThemeService::ThemeService()
161 rb_(ResourceBundle::GetSharedInstance()),
163 installed_pending_load_id_(kDefaultThemeID
),
164 number_of_infobars_(0),
165 weak_ptr_factory_(this) {
168 ThemeService::~ThemeService() {
169 FreePlatformCaches();
172 void ThemeService::Init(Profile
* profile
) {
173 DCHECK(CalledOnValidThread());
179 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
,
180 content::Source
<Profile
>(profile_
));
182 theme_syncable_service_
.reset(new ThemeSyncableService(profile_
, this));
185 gfx::Image
ThemeService::GetImageNamed(int id
) const {
186 DCHECK(CalledOnValidThread());
189 if (theme_supplier_
.get())
190 image
= theme_supplier_
->GetImageNamed(id
);
193 image
= rb_
.GetNativeImageNamed(id
);
198 bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
202 bool ThemeService::UsingSystemTheme() const {
203 return UsingDefaultTheme();
206 gfx::ImageSkia
* ThemeService::GetImageSkiaNamed(int id
) const {
207 gfx::Image image
= GetImageNamed(id
);
210 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns
211 // its images const. GetImageSkiaNamed() also should but has many callsites.
212 return const_cast<gfx::ImageSkia
*>(image
.ToImageSkia());
215 SkColor
ThemeService::GetColor(int id
) const {
216 DCHECK(CalledOnValidThread());
218 if (theme_supplier_
.get() && theme_supplier_
->GetColor(id
, &color
))
221 // For backward compat with older themes, some newer colors are generated from
222 // older ones if they are missing.
224 case Properties::COLOR_NTP_SECTION_HEADER_TEXT
:
225 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.30);
226 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER
:
227 return GetColor(Properties::COLOR_NTP_TEXT
);
228 case Properties::COLOR_NTP_SECTION_HEADER_RULE
:
229 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.70);
230 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT
:
231 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.86);
232 case Properties::COLOR_NTP_TEXT_LIGHT
:
233 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.40);
234 case Properties::COLOR_TAB_ICON
: {
236 bool found_color
= ui::CommonThemeGetSystemColor(
237 ui::NativeTheme::kColorId_ChromeIconGrey
, &base_color
);
239 color_utils::HSL hsl
= GetTint(Properties::TINT_BUTTONS
);
240 return color_utils::HSLShift(base_color
, hsl
);
242 case Properties::COLOR_THROBBER_SPINNING
:
243 case Properties::COLOR_THROBBER_WAITING
: {
245 bool found_color
= ui::CommonThemeGetSystemColor(
246 id
== Properties::COLOR_THROBBER_SPINNING
247 ? ui::NativeTheme::kColorId_ThrobberSpinningColor
248 : ui::NativeTheme::kColorId_ThrobberWaitingColor
,
251 color_utils::HSL hsl
= GetTint(Properties::TINT_BUTTONS
);
252 return color_utils::HSLShift(base_color
, hsl
);
254 #if defined(ENABLE_SUPERVISED_USERS)
255 case Properties::COLOR_SUPERVISED_USER_LABEL
:
256 return color_utils::GetReadableColor(
258 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
));
259 case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
:
260 return color_utils::BlendTowardOppositeLuminance(
261 GetColor(Properties::COLOR_FRAME
), 0x80);
262 case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER
:
263 return color_utils::AlphaBlend(
264 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
),
268 case Properties::COLOR_STATUS_BAR_TEXT
: {
269 // A long time ago, we blended the toolbar and the tab text together to
270 // get the status bar text because, at the time, our text rendering in
271 // views couldn't do alpha blending. Even though this is no longer the
272 // case, this blending decision is built into the majority of themes that
273 // exist, and we must keep doing it.
274 SkColor toolbar_color
= GetColor(Properties::COLOR_TOOLBAR
);
275 SkColor text_color
= GetColor(Properties::COLOR_TAB_TEXT
);
276 return SkColorSetARGB(
277 SkColorGetA(text_color
),
278 (SkColorGetR(text_color
) + SkColorGetR(toolbar_color
)) / 2,
279 (SkColorGetG(text_color
) + SkColorGetR(toolbar_color
)) / 2,
280 (SkColorGetB(text_color
) + SkColorGetR(toolbar_color
)) / 2);
284 return Properties::GetDefaultColor(id
);
287 int ThemeService::GetDisplayProperty(int id
) const {
289 if (theme_supplier_
.get() &&
290 theme_supplier_
->GetDisplayProperty(id
, &result
)) {
294 if (id
== Properties::NTP_LOGO_ALTERNATE
) {
295 if (UsingDefaultTheme() || UsingSystemTheme())
296 return 0; // Colorful logo.
298 if (HasCustomImage(IDR_THEME_NTP_BACKGROUND
))
299 return 1; // White logo.
301 SkColor background_color
= GetColor(Properties::COLOR_NTP_BACKGROUND
);
302 return IsColorGrayscale(background_color
) ? 0 : 1;
305 return Properties::GetDefaultDisplayProperty(id
);
308 bool ThemeService::ShouldUseNativeFrame() const {
309 if (HasCustomImage(IDR_THEME_FRAME
))
312 return ui::win::IsAeroGlassEnabled();
318 bool ThemeService::HasCustomImage(int id
) const {
319 return BrowserThemePack::IsPersistentImageID(id
) &&
320 theme_supplier_
&& theme_supplier_
->HasCustomImage(id
);
323 base::RefCountedMemory
* ThemeService::GetRawData(
325 ui::ScaleFactor scale_factor
) const {
326 // Check to see whether we should substitute some images.
327 int ntp_alternate
= GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE
);
328 if (id
== IDR_PRODUCT_LOGO
&& ntp_alternate
!= 0)
329 id
= IDR_PRODUCT_LOGO_WHITE
;
331 base::RefCountedMemory
* data
= nullptr;
332 if (theme_supplier_
.get())
333 data
= theme_supplier_
->GetRawData(id
, scale_factor
);
335 data
= rb_
.LoadDataResourceBytesForScale(id
, ui::SCALE_FACTOR_100P
);
340 void ThemeService::Shutdown() {
341 #if defined(ENABLE_EXTENSIONS)
342 theme_observer_
.reset();
346 void ThemeService::Observe(int type
,
347 const content::NotificationSource
& source
,
348 const content::NotificationDetails
& details
) {
349 using content::Details
;
351 case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
:
352 registrar_
.Remove(this,
353 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
,
354 content::Source
<Profile
>(profile_
));
355 OnExtensionServiceReady();
357 case extensions::NOTIFICATION_EXTENSION_ENABLED
: {
358 const Extension
* extension
= Details
<const Extension
>(details
).ptr();
359 if (extension
->is_theme())
368 void ThemeService::SetTheme(const Extension
* extension
) {
369 DCHECK(extension
->is_theme());
370 ExtensionService
* service
=
371 extensions::ExtensionSystem::Get(profile_
)->extension_service();
372 if (!service
->IsExtensionEnabled(extension
->id())) {
373 // |extension| is disabled when reverting to the previous theme via an
375 service
->EnableExtension(extension
->id());
376 // Enabling the extension will call back to SetTheme().
380 std::string previous_theme_id
= GetThemeID();
382 // Clear our image cache.
383 FreePlatformCaches();
385 BuildFromExtension(extension
);
386 SaveThemeID(extension
->id());
388 NotifyThemeChanged();
389 content::RecordAction(UserMetricsAction("Themes_Installed"));
391 if (previous_theme_id
!= kDefaultThemeID
&&
392 previous_theme_id
!= extension
->id()) {
393 // Disable the old theme.
394 service
->DisableExtension(previous_theme_id
,
395 extensions::Extension::DISABLE_USER_ACTION
);
399 void ThemeService::SetCustomDefaultTheme(
400 scoped_refptr
<CustomThemeSupplier
> theme_supplier
) {
402 SwapThemeSupplier(theme_supplier
);
403 NotifyThemeChanged();
406 bool ThemeService::ShouldInitWithSystemTheme() const {
410 void ThemeService::RemoveUnusedThemes(bool ignore_infobars
) {
411 // We do not want to garbage collect themes on startup (|ready_| is false).
412 // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
413 if (!profile_
|| !ready_
)
415 if (!ignore_infobars
&& number_of_infobars_
!= 0)
418 ExtensionService
* service
=
419 extensions::ExtensionSystem::Get(profile_
)->extension_service();
423 std::string current_theme
= GetThemeID();
424 std::vector
<std::string
> remove_list
;
425 scoped_ptr
<const extensions::ExtensionSet
> extensions(
426 extensions::ExtensionRegistry::Get(profile_
)
427 ->GenerateInstalledExtensionsSet());
428 extensions::ExtensionPrefs
* prefs
= extensions::ExtensionPrefs::Get(profile_
);
429 for (extensions::ExtensionSet::const_iterator it
= extensions
->begin();
430 it
!= extensions
->end(); ++it
) {
431 const extensions::Extension
* extension
= it
->get();
432 if (extension
->is_theme() &&
433 extension
->id() != current_theme
) {
434 // Only uninstall themes which are not disabled or are disabled with
435 // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
436 // themes because externally installed themes are initially disabled.
437 int disable_reason
= prefs
->GetDisableReasons(extension
->id());
438 if (!prefs
->IsExtensionDisabled(extension
->id()) ||
439 disable_reason
== Extension::DISABLE_USER_ACTION
) {
440 remove_list
.push_back((*it
)->id());
444 // TODO: Garbage collect all unused themes. This method misses themes which
445 // are installed but not loaded because they are blacklisted by a management
448 for (size_t i
= 0; i
< remove_list
.size(); ++i
) {
449 service
->UninstallExtension(remove_list
[i
],
450 extensions::UNINSTALL_REASON_ORPHANED_THEME
,
451 base::Bind(&base::DoNothing
), nullptr);
455 void ThemeService::UseDefaultTheme() {
457 content::RecordAction(UserMetricsAction("Themes_Reset"));
458 #if defined(ENABLE_SUPERVISED_USERS)
459 if (IsSupervisedUser()) {
460 SetSupervisedUserTheme();
465 NotifyThemeChanged();
468 void ThemeService::UseSystemTheme() {
472 bool ThemeService::UsingDefaultTheme() const {
473 std::string id
= GetThemeID();
474 return id
== ThemeService::kDefaultThemeID
||
475 id
== kDefaultThemeGalleryID
;
478 std::string
ThemeService::GetThemeID() const {
479 return profile_
->GetPrefs()->GetString(prefs::kCurrentThemeID
);
482 color_utils::HSL
ThemeService::GetTint(int id
) const {
483 DCHECK(CalledOnValidThread());
485 color_utils::HSL hsl
;
486 if (theme_supplier_
.get() && theme_supplier_
->GetTint(id
, &hsl
))
489 return ThemeProperties::GetDefaultTint(id
);
492 void ThemeService::ClearAllThemeData() {
496 SwapThemeSupplier(nullptr);
498 // Clear our image cache.
499 FreePlatformCaches();
501 profile_
->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename
);
502 SaveThemeID(kDefaultThemeID
);
504 // There should be no more infobars. This may not be the case because of
505 // http://crbug.com/62154
506 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
507 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
508 base::ThreadTaskRunnerHandle::Get()->PostTask(
509 FROM_HERE
, base::Bind(&ThemeService::RemoveUnusedThemes
,
510 weak_ptr_factory_
.GetWeakPtr(), true));
513 void ThemeService::LoadThemePrefs() {
514 PrefService
* prefs
= profile_
->GetPrefs();
516 std::string current_id
= GetThemeID();
517 if (current_id
== kDefaultThemeID
) {
518 #if defined(ENABLE_SUPERVISED_USERS)
519 // Supervised users have a different default theme.
520 if (IsSupervisedUser()) {
521 SetSupervisedUserTheme();
526 if (ShouldInitWithSystemTheme())
534 bool loaded_pack
= false;
536 // If we don't have a file pack, we're updating from an old version.
537 base::FilePath path
= prefs
->GetFilePath(prefs::kCurrentThemePackFilename
);
538 if (path
!= base::FilePath()) {
539 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path
, current_id
));
540 loaded_pack
= theme_supplier_
.get() != nullptr;
544 content::RecordAction(UserMetricsAction("Themes.Loaded"));
547 // Else: wait for the extension service to be ready so that the theme pack
548 // can be recreated from the extension.
551 void ThemeService::NotifyThemeChanged() {
555 DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
557 content::NotificationService
* service
=
558 content::NotificationService::current();
559 service
->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
560 content::Source
<ThemeService
>(this),
561 content::NotificationService::NoDetails());
562 #if defined(OS_MACOSX)
563 NotifyPlatformThemeChanged();
566 // Notify sync that theme has changed.
567 if (theme_syncable_service_
.get()) {
568 theme_syncable_service_
->OnThemeChange();
572 #if defined(USE_AURA)
573 void ThemeService::FreePlatformCaches() {
574 // Views (Skia) has no platform image cache to clear.
578 void ThemeService::OnExtensionServiceReady() {
580 // If the ThemeService is not ready yet, the custom theme data pack needs to
581 // be recreated from the extension.
585 // Send notification in case anyone requested data and cached it when the
586 // theme service was not ready yet.
587 NotifyThemeChanged();
590 #if defined(ENABLE_EXTENSIONS)
591 theme_observer_
.reset(new ThemeObserver(this));
595 extensions::NOTIFICATION_EXTENSION_ENABLED
,
596 content::Source
<Profile
>(profile_
));
598 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
599 FROM_HERE
, base::Bind(&ThemeService::RemoveUnusedThemes
,
600 weak_ptr_factory_
.GetWeakPtr(), false),
601 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay
));
604 void ThemeService::MigrateTheme() {
605 // TODO(erg): We need to pop up a dialog informing the user that their
606 // theme is being migrated.
607 ExtensionService
* service
=
608 extensions::ExtensionSystem::Get(profile_
)->extension_service();
609 const Extension
* extension
=
610 service
? service
->GetExtensionById(GetThemeID(), false) : nullptr;
612 DLOG(ERROR
) << "Migrating theme";
613 BuildFromExtension(extension
);
614 content::RecordAction(UserMetricsAction("Themes.Migrated"));
616 DLOG(ERROR
) << "Theme is mysteriously gone.";
618 content::RecordAction(UserMetricsAction("Themes.Gone"));
622 void ThemeService::SwapThemeSupplier(
623 scoped_refptr
<CustomThemeSupplier
> theme_supplier
) {
624 if (theme_supplier_
.get())
625 theme_supplier_
->StopUsingTheme();
626 theme_supplier_
= theme_supplier
;
627 if (theme_supplier_
.get())
628 theme_supplier_
->StartUsingTheme();
631 void ThemeService::SavePackName(const base::FilePath
& pack_path
) {
632 profile_
->GetPrefs()->SetFilePath(
633 prefs::kCurrentThemePackFilename
, pack_path
);
636 void ThemeService::SaveThemeID(const std::string
& id
) {
637 profile_
->GetPrefs()->SetString(prefs::kCurrentThemeID
, id
);
640 void ThemeService::BuildFromExtension(const Extension
* extension
) {
641 scoped_refptr
<BrowserThemePack
> pack(
642 BrowserThemePack::BuildFromExtension(extension
));
644 // TODO(erg): We've failed to install the theme; perhaps we should tell the
645 // user? http://crbug.com/34780
646 LOG(ERROR
) << "Could not load theme.";
650 ExtensionService
* service
=
651 extensions::ExtensionSystem::Get(profile_
)->extension_service();
655 // Write the packed file to disk.
656 base::FilePath pack_path
=
657 extension
->path().Append(chrome::kThemePackFilename
);
658 service
->GetFileTaskRunner()->PostTask(
660 base::Bind(&WritePackToDiskCallback
, pack
, pack_path
));
662 SavePackName(pack_path
);
663 SwapThemeSupplier(pack
);
666 #if defined(ENABLE_SUPERVISED_USERS)
667 bool ThemeService::IsSupervisedUser() const {
668 return profile_
->IsSupervised();
671 void ThemeService::SetSupervisedUserTheme() {
672 SetCustomDefaultTheme(new SupervisedUserTheme
);
676 void ThemeService::OnInfobarDisplayed() {
677 number_of_infobars_
++;
680 void ThemeService::OnInfobarDestroyed() {
681 number_of_infobars_
--;
683 if (number_of_infobars_
== 0)
684 RemoveUnusedThemes(false);
687 ThemeSyncableService
* ThemeService::GetThemeSyncableService() const {
688 return theme_syncable_service_
.get();