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/themes/browser_theme_pack.h"
20 #include "chrome/browser/themes/custom_theme_supplier.h"
21 #include "chrome/browser/themes/theme_properties.h"
22 #include "chrome/browser/themes/theme_syncable_service.h"
23 #include "chrome/common/chrome_constants.h"
24 #include "chrome/common/pref_names.h"
25 #include "content/public/browser/notification_service.h"
26 #include "content/public/browser/user_metrics.h"
27 #include "extensions/browser/extension_prefs.h"
28 #include "extensions/browser/extension_registry.h"
29 #include "extensions/browser/extension_system.h"
30 #include "extensions/browser/uninstall_reason.h"
31 #include "extensions/common/extension.h"
32 #include "extensions/common/extension_set.h"
33 #include "grit/theme_resources.h"
34 #include "ui/base/layout.h"
35 #include "ui/base/resource/resource_bundle.h"
36 #include "ui/gfx/image/image_skia.h"
38 #if defined(ENABLE_SUPERVISED_USERS)
39 #include "chrome/browser/supervised_user/supervised_user_theme.h"
43 #include "ui/base/win/shell.h"
46 using base::UserMetricsAction
;
47 using content::BrowserThread
;
48 using extensions::Extension
;
49 using extensions::UnloadedExtensionInfo
;
50 using ui::ResourceBundle
;
52 typedef ThemeProperties Properties
;
54 // The default theme if we haven't installed a theme yet or if we've clicked
55 // the "Use Classic" button.
56 const char* ThemeService::kDefaultThemeID
= "";
60 // The default theme if we've gone to the theme gallery and installed the
61 // "Default" theme. We have to detect this case specifically. (By the time we
62 // realize we've installed the default theme, we already have an extension
63 // unpacked on the filesystem.)
64 const char* kDefaultThemeGalleryID
= "hkacjpbfdknhflllbcmjibkdeoafencn";
66 // Wait this many seconds after startup to garbage collect unused themes.
67 // Removing unused themes is done after a delay because there is no
68 // reason to do it at startup.
69 // ExtensionService::GarbageCollectExtensions() does something similar.
70 const int kRemoveUnusedThemesStartupDelay
= 30;
72 SkColor
IncreaseLightness(SkColor color
, double percent
) {
73 color_utils::HSL result
;
74 color_utils::SkColorToHSL(color
, &result
);
75 result
.l
+= (1 - result
.l
) * percent
;
76 return color_utils::HSLToSkColor(result
, SkColorGetA(color
));
79 // Writes the theme pack to disk on a separate thread.
80 void WritePackToDiskCallback(BrowserThemePack
* pack
,
81 const base::FilePath
& path
) {
82 if (!pack
->WriteToDisk(path
))
83 NOTREACHED() << "Could not write theme pack to disk";
86 // Heuristic to determine if color is grayscale. This is used to decide whether
87 // to use the colorful or white logo, if a theme fails to specify which.
88 bool IsColorGrayscale(SkColor color
) {
89 const int kChannelTolerance
= 9;
90 int r
= SkColorGetR(color
);
91 int g
= SkColorGetG(color
);
92 int b
= SkColorGetB(color
);
93 int range
= std::max(r
, std::max(g
, b
)) - std::min(r
, std::min(g
, b
));
94 return range
< kChannelTolerance
;
99 ThemeService::ThemeService()
101 rb_(ResourceBundle::GetSharedInstance()),
103 installed_pending_load_id_(kDefaultThemeID
),
104 number_of_infobars_(0),
105 weak_ptr_factory_(this) {
108 ThemeService::~ThemeService() {
109 FreePlatformCaches();
112 void ThemeService::Init(Profile
* profile
) {
113 DCHECK(CalledOnValidThread());
119 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
,
120 content::Source
<Profile
>(profile_
));
122 theme_syncable_service_
.reset(new ThemeSyncableService(profile_
, this));
125 gfx::Image
ThemeService::GetImageNamed(int id
) const {
126 DCHECK(CalledOnValidThread());
129 if (theme_supplier_
.get())
130 image
= theme_supplier_
->GetImageNamed(id
);
133 image
= rb_
.GetNativeImageNamed(id
);
138 bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
142 bool ThemeService::UsingSystemTheme() const {
143 return UsingDefaultTheme();
146 gfx::ImageSkia
* ThemeService::GetImageSkiaNamed(int id
) const {
147 gfx::Image image
= GetImageNamed(id
);
150 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns
151 // its images const. GetImageSkiaNamed() also should but has many callsites.
152 return const_cast<gfx::ImageSkia
*>(image
.ToImageSkia());
155 SkColor
ThemeService::GetColor(int id
) const {
156 DCHECK(CalledOnValidThread());
158 if (theme_supplier_
.get() && theme_supplier_
->GetColor(id
, &color
))
161 // For backward compat with older themes, some newer colors are generated from
162 // older ones if they are missing.
164 case Properties::COLOR_NTP_SECTION_HEADER_TEXT
:
165 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.30);
166 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER
:
167 return GetColor(Properties::COLOR_NTP_TEXT
);
168 case Properties::COLOR_NTP_SECTION_HEADER_RULE
:
169 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.70);
170 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT
:
171 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.86);
172 case Properties::COLOR_NTP_TEXT_LIGHT
:
173 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.40);
174 #if defined(ENABLE_SUPERVISED_USERS)
175 case Properties::COLOR_SUPERVISED_USER_LABEL
:
176 return color_utils::GetReadableColor(
178 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
));
179 case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
:
180 return color_utils::BlendTowardOppositeLuminance(
181 GetColor(Properties::COLOR_FRAME
), 0x80);
182 case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER
:
183 return color_utils::AlphaBlend(
184 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND
),
188 case Properties::COLOR_STATUS_BAR_TEXT
: {
189 // A long time ago, we blended the toolbar and the tab text together to
190 // get the status bar text because, at the time, our text rendering in
191 // views couldn't do alpha blending. Even though this is no longer the
192 // case, this blending decision is built into the majority of themes that
193 // exist, and we must keep doing it.
194 SkColor toolbar_color
= GetColor(Properties::COLOR_TOOLBAR
);
195 SkColor text_color
= GetColor(Properties::COLOR_TAB_TEXT
);
196 return SkColorSetARGB(
197 SkColorGetA(text_color
),
198 (SkColorGetR(text_color
) + SkColorGetR(toolbar_color
)) / 2,
199 (SkColorGetG(text_color
) + SkColorGetR(toolbar_color
)) / 2,
200 (SkColorGetB(text_color
) + SkColorGetR(toolbar_color
)) / 2);
204 return Properties::GetDefaultColor(id
);
207 int ThemeService::GetDisplayProperty(int id
) const {
209 if (theme_supplier_
.get() &&
210 theme_supplier_
->GetDisplayProperty(id
, &result
)) {
214 if (id
== Properties::NTP_LOGO_ALTERNATE
) {
215 if (UsingDefaultTheme() || UsingSystemTheme())
216 return 0; // Colorful logo.
218 if (HasCustomImage(IDR_THEME_NTP_BACKGROUND
))
219 return 1; // White logo.
221 SkColor background_color
= GetColor(Properties::COLOR_NTP_BACKGROUND
);
222 return IsColorGrayscale(background_color
) ? 0 : 1;
225 return Properties::GetDefaultDisplayProperty(id
);
228 bool ThemeService::ShouldUseNativeFrame() const {
229 if (HasCustomImage(IDR_THEME_FRAME
))
232 return ui::win::IsAeroGlassEnabled();
238 bool ThemeService::HasCustomImage(int id
) const {
239 return BrowserThemePack::IsPersistentImageID(id
) &&
240 theme_supplier_
&& theme_supplier_
->HasCustomImage(id
);
243 base::RefCountedMemory
* ThemeService::GetRawData(
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
;
251 base::RefCountedMemory
* data
= NULL
;
252 if (theme_supplier_
.get())
253 data
= theme_supplier_
->GetRawData(id
, scale_factor
);
255 data
= rb_
.LoadDataResourceBytesForScale(id
, ui::SCALE_FACTOR_100P
);
260 void ThemeService::Observe(int type
,
261 const content::NotificationSource
& source
,
262 const content::NotificationDetails
& details
) {
263 using content::Details
;
265 case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
:
266 registrar_
.Remove(this,
267 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED
,
268 content::Source
<Profile
>(profile_
));
269 OnExtensionServiceReady();
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(
275 if (installed_details
->extension
->is_theme())
276 installed_pending_load_id_
= installed_details
->extension
->id();
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()) {
286 installed_pending_load_id_
= kDefaultThemeID
;
289 case extensions::NOTIFICATION_EXTENSION_ENABLED
: {
290 const Extension
* extension
= Details
<const Extension
>(details
).ptr();
291 if (extension
->is_theme())
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
->reason
!= UnloadedExtensionInfo::REASON_LOCK_ALL
&&
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
->get();
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 defined(ENABLE_SUPERVISED_USERS)
400 if (IsSupervisedUser()) {
401 SetSupervisedUserTheme();
406 NotifyThemeChanged();
409 void ThemeService::UseSystemTheme() {
413 bool ThemeService::UsingDefaultTheme() const {
414 std::string id
= GetThemeID();
415 return id
== ThemeService::kDefaultThemeID
||
416 id
== kDefaultThemeGalleryID
;
419 std::string
ThemeService::GetThemeID() const {
420 return profile_
->GetPrefs()->GetString(prefs::kCurrentThemeID
);
423 color_utils::HSL
ThemeService::GetTint(int id
) const {
424 DCHECK(CalledOnValidThread());
426 color_utils::HSL hsl
;
427 if (theme_supplier_
.get() && theme_supplier_
->GetTint(id
, &hsl
))
430 return ThemeProperties::GetDefaultTint(id
);
433 void ThemeService::ClearAllThemeData() {
437 SwapThemeSupplier(NULL
);
439 // Clear our image cache.
440 FreePlatformCaches();
442 profile_
->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename
);
443 SaveThemeID(kDefaultThemeID
);
445 // There should be no more infobars. This may not be the case because of
446 // http://crbug.com/62154
447 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
448 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
449 base::MessageLoop::current()->PostTask(FROM_HERE
,
450 base::Bind(&ThemeService::RemoveUnusedThemes
,
451 weak_ptr_factory_
.GetWeakPtr(),
455 void ThemeService::LoadThemePrefs() {
456 PrefService
* prefs
= profile_
->GetPrefs();
458 std::string current_id
= GetThemeID();
459 if (current_id
== kDefaultThemeID
) {
460 #if defined(ENABLE_SUPERVISED_USERS)
461 // Supervised users have a different default theme.
462 if (IsSupervisedUser()) {
463 SetSupervisedUserTheme();
468 if (ShouldInitWithSystemTheme())
476 bool loaded_pack
= false;
478 // If we don't have a file pack, we're updating from an old version.
479 base::FilePath path
= prefs
->GetFilePath(prefs::kCurrentThemePackFilename
);
480 if (path
!= base::FilePath()) {
481 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path
, current_id
));
482 loaded_pack
= theme_supplier_
.get() != NULL
;
486 content::RecordAction(UserMetricsAction("Themes.Loaded"));
489 // Else: wait for the extension service to be ready so that the theme pack
490 // can be recreated from the extension.
493 void ThemeService::NotifyThemeChanged() {
497 DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
499 content::NotificationService
* service
=
500 content::NotificationService::current();
501 service
->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
502 content::Source
<ThemeService
>(this),
503 content::NotificationService::NoDetails());
504 #if defined(OS_MACOSX)
505 NotifyPlatformThemeChanged();
508 // Notify sync that theme has changed.
509 if (theme_syncable_service_
.get()) {
510 theme_syncable_service_
->OnThemeChange();
514 #if defined(USE_AURA)
515 void ThemeService::FreePlatformCaches() {
516 // Views (Skia) has no platform image cache to clear.
520 void ThemeService::OnExtensionServiceReady() {
522 // If the ThemeService is not ready yet, the custom theme data pack needs to
523 // be recreated from the extension.
527 // Send notification in case anyone requested data and cached it when the
528 // theme service was not ready yet.
529 NotifyThemeChanged();
534 extensions::NOTIFICATION_EXTENSION_WILL_BE_INSTALLED_DEPRECATED
,
535 content::Source
<Profile
>(profile_
));
537 extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED
,
538 content::Source
<Profile
>(profile_
));
540 extensions::NOTIFICATION_EXTENSION_ENABLED
,
541 content::Source
<Profile
>(profile_
));
543 extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED
,
544 content::Source
<Profile
>(profile_
));
546 base::MessageLoop::current()->PostDelayedTask(FROM_HERE
,
547 base::Bind(&ThemeService::RemoveUnusedThemes
,
548 weak_ptr_factory_
.GetWeakPtr(),
550 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay
));
553 void ThemeService::MigrateTheme() {
554 // TODO(erg): We need to pop up a dialog informing the user that their
555 // theme is being migrated.
556 ExtensionService
* service
=
557 extensions::ExtensionSystem::Get(profile_
)->extension_service();
558 const Extension
* extension
= service
?
559 service
->GetExtensionById(GetThemeID(), false) : NULL
;
561 DLOG(ERROR
) << "Migrating theme";
562 BuildFromExtension(extension
);
563 content::RecordAction(UserMetricsAction("Themes.Migrated"));
565 DLOG(ERROR
) << "Theme is mysteriously gone.";
567 content::RecordAction(UserMetricsAction("Themes.Gone"));
571 void ThemeService::SwapThemeSupplier(
572 scoped_refptr
<CustomThemeSupplier
> theme_supplier
) {
573 if (theme_supplier_
.get())
574 theme_supplier_
->StopUsingTheme();
575 theme_supplier_
= theme_supplier
;
576 if (theme_supplier_
.get())
577 theme_supplier_
->StartUsingTheme();
580 void ThemeService::SavePackName(const base::FilePath
& pack_path
) {
581 profile_
->GetPrefs()->SetFilePath(
582 prefs::kCurrentThemePackFilename
, pack_path
);
585 void ThemeService::SaveThemeID(const std::string
& id
) {
586 profile_
->GetPrefs()->SetString(prefs::kCurrentThemeID
, id
);
589 void ThemeService::BuildFromExtension(const Extension
* extension
) {
590 scoped_refptr
<BrowserThemePack
> pack(
591 BrowserThemePack::BuildFromExtension(extension
));
593 // TODO(erg): We've failed to install the theme; perhaps we should tell the
594 // user? http://crbug.com/34780
595 LOG(ERROR
) << "Could not load theme.";
599 ExtensionService
* service
=
600 extensions::ExtensionSystem::Get(profile_
)->extension_service();
604 // Write the packed file to disk.
605 base::FilePath pack_path
=
606 extension
->path().Append(chrome::kThemePackFilename
);
607 service
->GetFileTaskRunner()->PostTask(
609 base::Bind(&WritePackToDiskCallback
, pack
, pack_path
));
611 SavePackName(pack_path
);
612 SwapThemeSupplier(pack
);
615 #if defined(ENABLE_SUPERVISED_USERS)
616 bool ThemeService::IsSupervisedUser() const {
617 return profile_
->IsSupervised();
620 void ThemeService::SetSupervisedUserTheme() {
621 SetCustomDefaultTheme(new SupervisedUserTheme
);
625 void ThemeService::OnInfobarDisplayed() {
626 number_of_infobars_
++;
629 void ThemeService::OnInfobarDestroyed() {
630 number_of_infobars_
--;
632 if (number_of_infobars_
== 0)
633 RemoveUnusedThemes(false);
636 ThemeSyncableService
* ThemeService::GetThemeSyncableService() const {
637 return theme_syncable_service_
.get();