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"
8 #include "base/memory/ref_counted_memory.h"
9 #include "base/message_loop/message_loop.h"
10 #include "base/prefs/pref_service.h"
11 #include "base/sequenced_task_runner.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/managed_mode/managed_user_theme.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/themes/browser_theme_pack.h"
19 #include "chrome/browser/themes/custom_theme_supplier.h"
20 #include "chrome/browser/themes/theme_properties.h"
21 #include "chrome/browser/themes/theme_syncable_service.h"
22 #include "chrome/common/chrome_constants.h"
23 #include "chrome/common/pref_names.h"
24 #include "content/public/browser/notification_service.h"
25 #include "content/public/browser/user_metrics.h"
26 #include "extensions/browser/extension_prefs.h"
27 #include "extensions/browser/extension_registry.h"
28 #include "extensions/browser/extension_system.h"
29 #include "extensions/common/extension.h"
30 #include "extensions/common/extension_set.h"
31 #include "grit/theme_resources.h"
32 #include "grit/ui_resources.h"
33 #include "ui/base/layout.h"
34 #include "ui/base/resource/resource_bundle.h"
35 #include "ui/gfx/image/image_skia.h"
38 #include "ui/base/win/shell.h"
41 using base::UserMetricsAction
;
42 using content::BrowserThread
;
43 using extensions::Extension
;
44 using extensions::UnloadedExtensionInfo
;
45 using ui::ResourceBundle
;
47 typedef ThemeProperties Properties
;
49 // The default theme if we haven't installed a theme yet or if we've clicked
50 // the "Use Classic" button.
51 const char* ThemeService::kDefaultThemeID
= "";
55 // The default theme if we've gone to the theme gallery and installed the
56 // "Default" theme. We have to detect this case specifically. (By the time we
57 // realize we've installed the default theme, we already have an extension
58 // unpacked on the filesystem.)
59 const char* kDefaultThemeGalleryID
= "hkacjpbfdknhflllbcmjibkdeoafencn";
61 // Wait this many seconds after startup to garbage collect unused themes.
62 // Removing unused themes is done after a delay because there is no
63 // reason to do it at startup.
64 // ExtensionService::GarbageCollectExtensions() does something similar.
65 const int kRemoveUnusedThemesStartupDelay
= 30;
67 SkColor
IncreaseLightness(SkColor color
, double percent
) {
68 color_utils::HSL result
;
69 color_utils::SkColorToHSL(color
, &result
);
70 result
.l
+= (1 - result
.l
) * percent
;
71 return color_utils::HSLToSkColor(result
, SkColorGetA(color
));
74 // Writes the theme pack to disk on a separate thread.
75 void WritePackToDiskCallback(BrowserThemePack
* pack
,
76 const base::FilePath
& path
) {
77 if (!pack
->WriteToDisk(path
))
78 NOTREACHED() << "Could not write theme pack to disk";
83 ThemeService::ThemeService()
85 rb_(ResourceBundle::GetSharedInstance()),
87 installed_pending_load_id_(kDefaultThemeID
),
88 number_of_infobars_(0),
89 weak_ptr_factory_(this) {
92 ThemeService::~ThemeService() {
96 void ThemeService::Init(Profile
* profile
) {
97 DCHECK(CalledOnValidThread());
103 chrome::NOTIFICATION_EXTENSIONS_READY
,
104 content::Source
<Profile
>(profile_
));
106 theme_syncable_service_
.reset(new ThemeSyncableService(profile_
, this));
109 gfx::Image
ThemeService::GetImageNamed(int id
) const {
110 DCHECK(CalledOnValidThread());
113 if (theme_supplier_
.get())
114 image
= theme_supplier_
->GetImageNamed(id
);
117 image
= rb_
.GetNativeImageNamed(id
);
122 bool ThemeService::UsingNativeTheme() const {
123 return UsingDefaultTheme();
126 gfx::ImageSkia
* ThemeService::GetImageSkiaNamed(int id
) const {
127 gfx::Image image
= GetImageNamed(id
);
130 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns
131 // its images const. GetImageSkiaNamed() also should but has many callsites.
132 return const_cast<gfx::ImageSkia
*>(image
.ToImageSkia());
135 SkColor
ThemeService::GetColor(int id
) const {
136 DCHECK(CalledOnValidThread());
138 if (theme_supplier_
.get() && theme_supplier_
->GetColor(id
, &color
))
141 // For backward compat with older themes, some newer colors are generated from
142 // older ones if they are missing.
144 case Properties::COLOR_NTP_SECTION_HEADER_TEXT
:
145 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.30);
146 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER
:
147 return GetColor(Properties::COLOR_NTP_TEXT
);
148 case Properties::COLOR_NTP_SECTION_HEADER_RULE
:
149 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.70);
150 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT
:
151 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.86);
152 case Properties::COLOR_NTP_TEXT_LIGHT
:
153 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT
), 0.40);
154 case Properties::COLOR_MANAGED_USER_LABEL
:
155 return color_utils::GetReadableColor(
157 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND
));
158 case Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND
:
159 return color_utils::BlendTowardOppositeLuminance(
160 GetColor(Properties::COLOR_FRAME
), 0x80);
161 case Properties::COLOR_MANAGED_USER_LABEL_BORDER
:
162 return color_utils::AlphaBlend(
163 GetColor(Properties::COLOR_MANAGED_USER_LABEL_BACKGROUND
),
166 case Properties::COLOR_STATUS_BAR_TEXT
: {
167 // A long time ago, we blended the toolbar and the tab text together to
168 // get the status bar text because, at the time, our text rendering in
169 // views couldn't do alpha blending. Even though this is no longer the
170 // case, this blending decision is built into the majority of themes that
171 // exist, and we must keep doing it.
172 SkColor toolbar_color
= GetColor(Properties::COLOR_TOOLBAR
);
173 SkColor text_color
= GetColor(Properties::COLOR_TAB_TEXT
);
174 return SkColorSetARGB(
175 SkColorGetA(text_color
),
176 (SkColorGetR(text_color
) + SkColorGetR(toolbar_color
)) / 2,
177 (SkColorGetG(text_color
) + SkColorGetR(toolbar_color
)) / 2,
178 (SkColorGetB(text_color
) + SkColorGetR(toolbar_color
)) / 2);
182 return Properties::GetDefaultColor(id
);
185 int ThemeService::GetDisplayProperty(int id
) const {
187 if (theme_supplier_
.get() &&
188 theme_supplier_
->GetDisplayProperty(id
, &result
)) {
192 if (id
== Properties::NTP_LOGO_ALTERNATE
&&
193 !UsingDefaultTheme() &&
194 !UsingNativeTheme()) {
195 // Use the alternate logo for themes from the web store except for
196 // |kDefaultThemeGalleryID|.
200 return Properties::GetDefaultDisplayProperty(id
);
203 bool ThemeService::ShouldUseNativeFrame() const {
204 if (HasCustomImage(IDR_THEME_FRAME
))
207 return ui::win::IsAeroGlassEnabled();
213 bool ThemeService::HasCustomImage(int id
) const {
214 if (!Properties::IsThemeableImage(id
))
217 if (theme_supplier_
.get())
218 return theme_supplier_
->HasCustomImage(id
);
223 base::RefCountedMemory
* ThemeService::GetRawData(
225 ui::ScaleFactor scale_factor
) const {
226 // Check to see whether we should substitute some images.
227 int ntp_alternate
= GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE
);
228 if (id
== IDR_PRODUCT_LOGO
&& ntp_alternate
!= 0)
229 id
= IDR_PRODUCT_LOGO_WHITE
;
231 base::RefCountedMemory
* data
= NULL
;
232 if (theme_supplier_
.get())
233 data
= theme_supplier_
->GetRawData(id
, scale_factor
);
235 data
= rb_
.LoadDataResourceBytesForScale(id
, ui::SCALE_FACTOR_100P
);
240 void ThemeService::Observe(int type
,
241 const content::NotificationSource
& source
,
242 const content::NotificationDetails
& details
) {
243 using content::Details
;
245 case chrome::NOTIFICATION_EXTENSIONS_READY
:
246 registrar_
.Remove(this, chrome::NOTIFICATION_EXTENSIONS_READY
,
247 content::Source
<Profile
>(profile_
));
248 OnExtensionServiceReady();
250 case chrome::NOTIFICATION_EXTENSION_INSTALLED
:
252 // The theme may be initially disabled. Wait till it is loaded (if ever).
253 Details
<const extensions::InstalledExtensionInfo
> installed_details(
255 if (installed_details
->extension
->is_theme())
256 installed_pending_load_id_
= installed_details
->extension
->id();
259 case chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED
:
261 const Extension
* extension
= Details
<const Extension
>(details
).ptr();
262 if (extension
->is_theme() &&
263 installed_pending_load_id_
!= kDefaultThemeID
&&
264 installed_pending_load_id_
== extension
->id()) {
267 installed_pending_load_id_
= kDefaultThemeID
;
270 case chrome::NOTIFICATION_EXTENSION_ENABLED
:
272 const Extension
* extension
= Details
<const Extension
>(details
).ptr();
273 if (extension
->is_theme())
277 case chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED
:
279 Details
<const UnloadedExtensionInfo
> unloaded_details(details
);
280 if (unloaded_details
->reason
!= UnloadedExtensionInfo::REASON_UPDATE
&&
281 unloaded_details
->extension
->is_theme() &&
282 unloaded_details
->extension
->id() == GetThemeID()) {
290 void ThemeService::SetTheme(const Extension
* extension
) {
291 DCHECK(extension
->is_theme());
292 ExtensionService
* service
=
293 extensions::ExtensionSystem::Get(profile_
)->extension_service();
294 if (!service
->IsExtensionEnabled(extension
->id())) {
295 // |extension| is disabled when reverting to the previous theme via an
297 service
->EnableExtension(extension
->id());
298 // Enabling the extension will call back to SetTheme().
302 std::string previous_theme_id
= GetThemeID();
304 // Clear our image cache.
305 FreePlatformCaches();
307 BuildFromExtension(extension
);
308 SaveThemeID(extension
->id());
310 NotifyThemeChanged();
311 content::RecordAction(UserMetricsAction("Themes_Installed"));
313 if (previous_theme_id
!= kDefaultThemeID
&&
314 previous_theme_id
!= extension
->id()) {
315 // Disable the old theme.
316 service
->DisableExtension(previous_theme_id
,
317 extensions::Extension::DISABLE_USER_ACTION
);
321 void ThemeService::SetCustomDefaultTheme(
322 scoped_refptr
<CustomThemeSupplier
> theme_supplier
) {
324 SwapThemeSupplier(theme_supplier
);
325 NotifyThemeChanged();
328 bool ThemeService::ShouldInitWithNativeTheme() const {
332 void ThemeService::RemoveUnusedThemes(bool ignore_infobars
) {
333 // We do not want to garbage collect themes on startup (|ready_| is false).
334 // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
335 if (!profile_
|| !ready_
)
337 if (!ignore_infobars
&& number_of_infobars_
!= 0)
340 ExtensionService
* service
=
341 extensions::ExtensionSystem::Get(profile_
)->extension_service();
345 std::string current_theme
= GetThemeID();
346 std::vector
<std::string
> remove_list
;
347 scoped_ptr
<const extensions::ExtensionSet
> extensions(
348 extensions::ExtensionRegistry::Get(profile_
)
349 ->GenerateInstalledExtensionsSet());
350 extensions::ExtensionPrefs
* prefs
= extensions::ExtensionPrefs::Get(profile_
);
351 for (extensions::ExtensionSet::const_iterator it
= extensions
->begin();
352 it
!= extensions
->end(); ++it
) {
353 const extensions::Extension
* extension
= *it
;
354 if (extension
->is_theme() &&
355 extension
->id() != current_theme
) {
356 // Only uninstall themes which are not disabled or are disabled with
357 // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
358 // themes because externally installed themes are initially disabled.
359 int disable_reason
= prefs
->GetDisableReasons(extension
->id());
360 if (!prefs
->IsExtensionDisabled(extension
->id()) ||
361 disable_reason
== Extension::DISABLE_USER_ACTION
) {
362 remove_list
.push_back((*it
)->id());
366 // TODO: Garbage collect all unused themes. This method misses themes which
367 // are installed but not loaded because they are blacklisted by a management
370 for (size_t i
= 0; i
< remove_list
.size(); ++i
)
371 service
->UninstallExtension(remove_list
[i
], false, NULL
);
374 void ThemeService::UseDefaultTheme() {
376 content::RecordAction(UserMetricsAction("Themes_Reset"));
377 if (IsManagedUser()) {
378 SetManagedUserTheme();
382 NotifyThemeChanged();
385 void ThemeService::SetNativeTheme() {
389 bool ThemeService::UsingDefaultTheme() const {
390 std::string id
= GetThemeID();
391 return id
== ThemeService::kDefaultThemeID
||
392 id
== kDefaultThemeGalleryID
;
395 std::string
ThemeService::GetThemeID() const {
396 return profile_
->GetPrefs()->GetString(prefs::kCurrentThemeID
);
399 color_utils::HSL
ThemeService::GetTint(int id
) const {
400 DCHECK(CalledOnValidThread());
402 color_utils::HSL hsl
;
403 if (theme_supplier_
.get() && theme_supplier_
->GetTint(id
, &hsl
))
406 return ThemeProperties::GetDefaultTint(id
);
409 void ThemeService::ClearAllThemeData() {
413 SwapThemeSupplier(NULL
);
415 // Clear our image cache.
416 FreePlatformCaches();
418 profile_
->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename
);
419 SaveThemeID(kDefaultThemeID
);
421 // There should be no more infobars. This may not be the case because of
422 // http://crbug.com/62154
423 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
424 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
425 base::MessageLoop::current()->PostTask(FROM_HERE
,
426 base::Bind(&ThemeService::RemoveUnusedThemes
,
427 weak_ptr_factory_
.GetWeakPtr(),
431 void ThemeService::LoadThemePrefs() {
432 PrefService
* prefs
= profile_
->GetPrefs();
434 std::string current_id
= GetThemeID();
435 if (current_id
== kDefaultThemeID
) {
436 // Managed users have a different default theme.
438 SetManagedUserTheme();
439 else if (ShouldInitWithNativeTheme())
447 bool loaded_pack
= false;
449 // If we don't have a file pack, we're updating from an old version.
450 base::FilePath path
= prefs
->GetFilePath(prefs::kCurrentThemePackFilename
);
451 if (path
!= base::FilePath()) {
452 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path
, current_id
));
453 loaded_pack
= theme_supplier_
.get() != NULL
;
457 content::RecordAction(UserMetricsAction("Themes.Loaded"));
460 // Else: wait for the extension service to be ready so that the theme pack
461 // can be recreated from the extension.
464 void ThemeService::NotifyThemeChanged() {
468 DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
470 content::NotificationService
* service
=
471 content::NotificationService::current();
472 service
->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
473 content::Source
<ThemeService
>(this),
474 content::NotificationService::NoDetails());
475 #if defined(OS_MACOSX)
476 NotifyPlatformThemeChanged();
479 // Notify sync that theme has changed.
480 if (theme_syncable_service_
.get()) {
481 theme_syncable_service_
->OnThemeChange();
485 #if defined(USE_AURA)
486 void ThemeService::FreePlatformCaches() {
487 // Views (Skia) has no platform image cache to clear.
491 void ThemeService::OnExtensionServiceReady() {
493 // If the ThemeService is not ready yet, the custom theme data pack needs to
494 // be recreated from the extension.
498 // Send notification in case anyone requested data and cached it when the
499 // theme service was not ready yet.
500 NotifyThemeChanged();
504 chrome::NOTIFICATION_EXTENSION_INSTALLED
,
505 content::Source
<Profile
>(profile_
));
507 chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED
,
508 content::Source
<Profile
>(profile_
));
510 chrome::NOTIFICATION_EXTENSION_ENABLED
,
511 content::Source
<Profile
>(profile_
));
513 chrome::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED
,
514 content::Source
<Profile
>(profile_
));
516 base::MessageLoop::current()->PostDelayedTask(FROM_HERE
,
517 base::Bind(&ThemeService::RemoveUnusedThemes
,
518 weak_ptr_factory_
.GetWeakPtr(),
520 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay
));
523 void ThemeService::MigrateTheme() {
524 // TODO(erg): We need to pop up a dialog informing the user that their
525 // theme is being migrated.
526 ExtensionService
* service
=
527 extensions::ExtensionSystem::Get(profile_
)->extension_service();
528 const Extension
* extension
= service
?
529 service
->GetExtensionById(GetThemeID(), false) : NULL
;
531 DLOG(ERROR
) << "Migrating theme";
532 BuildFromExtension(extension
);
533 content::RecordAction(UserMetricsAction("Themes.Migrated"));
535 DLOG(ERROR
) << "Theme is mysteriously gone.";
537 content::RecordAction(UserMetricsAction("Themes.Gone"));
541 void ThemeService::SwapThemeSupplier(
542 scoped_refptr
<CustomThemeSupplier
> theme_supplier
) {
543 if (theme_supplier_
.get())
544 theme_supplier_
->StopUsingTheme();
545 theme_supplier_
= theme_supplier
;
546 if (theme_supplier_
.get())
547 theme_supplier_
->StartUsingTheme();
550 void ThemeService::SavePackName(const base::FilePath
& pack_path
) {
551 profile_
->GetPrefs()->SetFilePath(
552 prefs::kCurrentThemePackFilename
, pack_path
);
555 void ThemeService::SaveThemeID(const std::string
& id
) {
556 profile_
->GetPrefs()->SetString(prefs::kCurrentThemeID
, id
);
559 void ThemeService::BuildFromExtension(const Extension
* extension
) {
560 scoped_refptr
<BrowserThemePack
> pack(
561 BrowserThemePack::BuildFromExtension(extension
));
563 // TODO(erg): We've failed to install the theme; perhaps we should tell the
564 // user? http://crbug.com/34780
565 LOG(ERROR
) << "Could not load theme.";
569 ExtensionService
* service
=
570 extensions::ExtensionSystem::Get(profile_
)->extension_service();
574 // Write the packed file to disk.
575 base::FilePath pack_path
=
576 extension
->path().Append(chrome::kThemePackFilename
);
577 service
->GetFileTaskRunner()->PostTask(
579 base::Bind(&WritePackToDiskCallback
, pack
, pack_path
));
581 SavePackName(pack_path
);
582 SwapThemeSupplier(pack
);
585 bool ThemeService::IsManagedUser() const {
586 return profile_
->IsManaged();
589 void ThemeService::SetManagedUserTheme() {
590 SetCustomDefaultTheme(new ManagedUserTheme
);
593 void ThemeService::OnInfobarDisplayed() {
594 number_of_infobars_
++;
597 void ThemeService::OnInfobarDestroyed() {
598 number_of_infobars_
--;
600 if (number_of_infobars_
== 0)
601 RemoveUnusedThemes(false);
604 ThemeSyncableService
* ThemeService::GetThemeSyncableService() const {
605 return theme_syncable_service_
.get();