Fire an error if a pref used in the UI is missing once all prefs are fetched.
[chromium-blink-merge.git] / chrome / browser / themes / theme_service.cc
blob0594475bd13b440bfe2886a84b641d82c1fdda3b
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"
7 #include <algorithm>
9 #include "base/bind.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_EXTENSIONS)
39 #include "extensions/browser/extension_registry_observer.h"
40 #endif
42 #if defined(ENABLE_SUPERVISED_USERS)
43 #include "chrome/browser/supervised_user/supervised_user_theme.h"
44 #endif
46 #if defined(OS_WIN)
47 #include "ui/base/win/shell.h"
48 #endif
50 using base::UserMetricsAction;
51 using content::BrowserThread;
52 using extensions::Extension;
53 using extensions::UnloadedExtensionInfo;
54 using ui::ResourceBundle;
56 typedef ThemeProperties Properties;
58 // The default theme if we haven't installed a theme yet or if we've clicked
59 // the "Use Classic" button.
60 const char* ThemeService::kDefaultThemeID = "";
62 namespace {
64 // The default theme if we've gone to the theme gallery and installed the
65 // "Default" theme. We have to detect this case specifically. (By the time we
66 // realize we've installed the default theme, we already have an extension
67 // unpacked on the filesystem.)
68 const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn";
70 // Wait this many seconds after startup to garbage collect unused themes.
71 // Removing unused themes is done after a delay because there is no
72 // reason to do it at startup.
73 // ExtensionService::GarbageCollectExtensions() does something similar.
74 const int kRemoveUnusedThemesStartupDelay = 30;
76 SkColor IncreaseLightness(SkColor color, double percent) {
77 color_utils::HSL result;
78 color_utils::SkColorToHSL(color, &result);
79 result.l += (1 - result.l) * percent;
80 return color_utils::HSLToSkColor(result, SkColorGetA(color));
83 // Writes the theme pack to disk on a separate thread.
84 void WritePackToDiskCallback(BrowserThemePack* pack,
85 const base::FilePath& path) {
86 if (!pack->WriteToDisk(path))
87 NOTREACHED() << "Could not write theme pack to disk";
90 // Heuristic to determine if color is grayscale. This is used to decide whether
91 // to use the colorful or white logo, if a theme fails to specify which.
92 bool IsColorGrayscale(SkColor color) {
93 const int kChannelTolerance = 9;
94 int r = SkColorGetR(color);
95 int g = SkColorGetG(color);
96 int b = SkColorGetB(color);
97 int range = std::max(r, std::max(g, b)) - std::min(r, std::min(g, b));
98 return range < kChannelTolerance;
101 } // namespace
103 #if defined(ENABLE_EXTENSIONS)
104 class ThemeService::ThemeObserver
105 : public extensions::ExtensionRegistryObserver {
106 public:
107 explicit ThemeObserver(ThemeService* service) : theme_service_(service) {
108 extensions::ExtensionRegistry::Get(theme_service_->profile_)
109 ->AddObserver(this);
112 ~ThemeObserver() override {
113 extensions::ExtensionRegistry::Get(theme_service_->profile_)
114 ->RemoveObserver(this);
117 private:
118 void OnExtensionWillBeInstalled(content::BrowserContext* browser_context,
119 const extensions::Extension* extension,
120 bool is_update,
121 bool from_ephemeral,
122 const std::string& old_name) override {
123 if (extension->is_theme()) {
124 // The theme may be initially disabled. Wait till it is loaded (if ever).
125 theme_service_->installed_pending_load_id_ = extension->id();
129 void OnExtensionLoaded(content::BrowserContext* browser_context,
130 const extensions::Extension* extension) override {
131 if (extension->is_theme() &&
132 theme_service_->installed_pending_load_id_ != kDefaultThemeID &&
133 theme_service_->installed_pending_load_id_ == extension->id()) {
134 theme_service_->SetTheme(extension);
136 theme_service_->installed_pending_load_id_ = kDefaultThemeID;
139 void OnExtensionUnloaded(
140 content::BrowserContext* browser_context,
141 const extensions::Extension* extension,
142 extensions::UnloadedExtensionInfo::Reason reason) override {
143 if (reason != extensions::UnloadedExtensionInfo::REASON_UPDATE &&
144 reason != extensions::UnloadedExtensionInfo::REASON_LOCK_ALL &&
145 extension->is_theme() &&
146 extension->id() == theme_service_->GetThemeID()) {
147 theme_service_->UseDefaultTheme();
151 ThemeService* theme_service_;
153 #endif // defined(ENABLE_EXTENSIONS)
155 ThemeService::ThemeService()
156 : ready_(false),
157 rb_(ResourceBundle::GetSharedInstance()),
158 profile_(nullptr),
159 installed_pending_load_id_(kDefaultThemeID),
160 number_of_infobars_(0),
161 weak_ptr_factory_(this) {
164 ThemeService::~ThemeService() {
165 FreePlatformCaches();
168 void ThemeService::Init(Profile* profile) {
169 DCHECK(CalledOnValidThread());
170 profile_ = profile;
172 LoadThemePrefs();
174 registrar_.Add(this,
175 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
176 content::Source<Profile>(profile_));
178 theme_syncable_service_.reset(new ThemeSyncableService(profile_, this));
181 gfx::Image ThemeService::GetImageNamed(int id) const {
182 DCHECK(CalledOnValidThread());
184 gfx::Image image;
185 if (theme_supplier_.get())
186 image = theme_supplier_->GetImageNamed(id);
188 if (image.IsEmpty())
189 image = rb_.GetNativeImageNamed(id);
191 return image;
194 bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
195 return false;
198 bool ThemeService::UsingSystemTheme() const {
199 return UsingDefaultTheme();
202 gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const {
203 gfx::Image image = GetImageNamed(id);
204 if (image.IsEmpty())
205 return nullptr;
206 // TODO(pkotwicz): Remove this const cast. The gfx::Image interface returns
207 // its images const. GetImageSkiaNamed() also should but has many callsites.
208 return const_cast<gfx::ImageSkia*>(image.ToImageSkia());
211 SkColor ThemeService::GetColor(int id) const {
212 DCHECK(CalledOnValidThread());
213 SkColor color;
214 if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color))
215 return color;
217 // For backward compat with older themes, some newer colors are generated from
218 // older ones if they are missing.
219 switch (id) {
220 case Properties::COLOR_NTP_SECTION_HEADER_TEXT:
221 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30);
222 case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER:
223 return GetColor(Properties::COLOR_NTP_TEXT);
224 case Properties::COLOR_NTP_SECTION_HEADER_RULE:
225 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70);
226 case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT:
227 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86);
228 case Properties::COLOR_NTP_TEXT_LIGHT:
229 return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40);
230 #if defined(ENABLE_SUPERVISED_USERS)
231 case Properties::COLOR_SUPERVISED_USER_LABEL:
232 return color_utils::GetReadableColor(
233 SK_ColorWHITE,
234 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND));
235 case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND:
236 return color_utils::BlendTowardOppositeLuminance(
237 GetColor(Properties::COLOR_FRAME), 0x80);
238 case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER:
239 return color_utils::AlphaBlend(
240 GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND),
241 SK_ColorBLACK,
242 230);
243 #endif
244 case Properties::COLOR_STATUS_BAR_TEXT: {
245 // A long time ago, we blended the toolbar and the tab text together to
246 // get the status bar text because, at the time, our text rendering in
247 // views couldn't do alpha blending. Even though this is no longer the
248 // case, this blending decision is built into the majority of themes that
249 // exist, and we must keep doing it.
250 SkColor toolbar_color = GetColor(Properties::COLOR_TOOLBAR);
251 SkColor text_color = GetColor(Properties::COLOR_TAB_TEXT);
252 return SkColorSetARGB(
253 SkColorGetA(text_color),
254 (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2,
255 (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2,
256 (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2);
260 return Properties::GetDefaultColor(id);
263 int ThemeService::GetDisplayProperty(int id) const {
264 int result = 0;
265 if (theme_supplier_.get() &&
266 theme_supplier_->GetDisplayProperty(id, &result)) {
267 return result;
270 if (id == Properties::NTP_LOGO_ALTERNATE) {
271 if (UsingDefaultTheme() || UsingSystemTheme())
272 return 0; // Colorful logo.
274 if (HasCustomImage(IDR_THEME_NTP_BACKGROUND))
275 return 1; // White logo.
277 SkColor background_color = GetColor(Properties::COLOR_NTP_BACKGROUND);
278 return IsColorGrayscale(background_color) ? 0 : 1;
281 return Properties::GetDefaultDisplayProperty(id);
284 bool ThemeService::ShouldUseNativeFrame() const {
285 if (HasCustomImage(IDR_THEME_FRAME))
286 return false;
287 #if defined(OS_WIN)
288 return ui::win::IsAeroGlassEnabled();
289 #else
290 return false;
291 #endif
294 bool ThemeService::HasCustomImage(int id) const {
295 return BrowserThemePack::IsPersistentImageID(id) &&
296 theme_supplier_ && theme_supplier_->HasCustomImage(id);
299 base::RefCountedMemory* ThemeService::GetRawData(
300 int id,
301 ui::ScaleFactor scale_factor) const {
302 // Check to see whether we should substitute some images.
303 int ntp_alternate = GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE);
304 if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0)
305 id = IDR_PRODUCT_LOGO_WHITE;
307 base::RefCountedMemory* data = nullptr;
308 if (theme_supplier_.get())
309 data = theme_supplier_->GetRawData(id, scale_factor);
310 if (!data)
311 data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P);
313 return data;
316 void ThemeService::Shutdown() {
317 #if defined(ENABLE_EXTENSIONS)
318 theme_observer_.reset();
319 #endif
322 void ThemeService::Observe(int type,
323 const content::NotificationSource& source,
324 const content::NotificationDetails& details) {
325 using content::Details;
326 switch (type) {
327 case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED:
328 registrar_.Remove(this,
329 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
330 content::Source<Profile>(profile_));
331 OnExtensionServiceReady();
332 break;
333 case extensions::NOTIFICATION_EXTENSION_ENABLED: {
334 const Extension* extension = Details<const Extension>(details).ptr();
335 if (extension->is_theme())
336 SetTheme(extension);
337 break;
339 default:
340 NOTREACHED();
344 void ThemeService::SetTheme(const Extension* extension) {
345 DCHECK(extension->is_theme());
346 ExtensionService* service =
347 extensions::ExtensionSystem::Get(profile_)->extension_service();
348 if (!service->IsExtensionEnabled(extension->id())) {
349 // |extension| is disabled when reverting to the previous theme via an
350 // infobar.
351 service->EnableExtension(extension->id());
352 // Enabling the extension will call back to SetTheme().
353 return;
356 std::string previous_theme_id = GetThemeID();
358 // Clear our image cache.
359 FreePlatformCaches();
361 BuildFromExtension(extension);
362 SaveThemeID(extension->id());
364 NotifyThemeChanged();
365 content::RecordAction(UserMetricsAction("Themes_Installed"));
367 if (previous_theme_id != kDefaultThemeID &&
368 previous_theme_id != extension->id()) {
369 // Disable the old theme.
370 service->DisableExtension(previous_theme_id,
371 extensions::Extension::DISABLE_USER_ACTION);
375 void ThemeService::SetCustomDefaultTheme(
376 scoped_refptr<CustomThemeSupplier> theme_supplier) {
377 ClearAllThemeData();
378 SwapThemeSupplier(theme_supplier);
379 NotifyThemeChanged();
382 bool ThemeService::ShouldInitWithSystemTheme() const {
383 return false;
386 void ThemeService::RemoveUnusedThemes(bool ignore_infobars) {
387 // We do not want to garbage collect themes on startup (|ready_| is false).
388 // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
389 if (!profile_ || !ready_)
390 return;
391 if (!ignore_infobars && number_of_infobars_ != 0)
392 return;
394 ExtensionService* service =
395 extensions::ExtensionSystem::Get(profile_)->extension_service();
396 if (!service)
397 return;
399 std::string current_theme = GetThemeID();
400 std::vector<std::string> remove_list;
401 scoped_ptr<const extensions::ExtensionSet> extensions(
402 extensions::ExtensionRegistry::Get(profile_)
403 ->GenerateInstalledExtensionsSet());
404 extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
405 for (extensions::ExtensionSet::const_iterator it = extensions->begin();
406 it != extensions->end(); ++it) {
407 const extensions::Extension* extension = it->get();
408 if (extension->is_theme() &&
409 extension->id() != current_theme) {
410 // Only uninstall themes which are not disabled or are disabled with
411 // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
412 // themes because externally installed themes are initially disabled.
413 int disable_reason = prefs->GetDisableReasons(extension->id());
414 if (!prefs->IsExtensionDisabled(extension->id()) ||
415 disable_reason == Extension::DISABLE_USER_ACTION) {
416 remove_list.push_back((*it)->id());
420 // TODO: Garbage collect all unused themes. This method misses themes which
421 // are installed but not loaded because they are blacklisted by a management
422 // policy provider.
424 for (size_t i = 0; i < remove_list.size(); ++i) {
425 service->UninstallExtension(remove_list[i],
426 extensions::UNINSTALL_REASON_ORPHANED_THEME,
427 base::Bind(&base::DoNothing), nullptr);
431 void ThemeService::UseDefaultTheme() {
432 if (ready_)
433 content::RecordAction(UserMetricsAction("Themes_Reset"));
434 #if defined(ENABLE_SUPERVISED_USERS)
435 if (IsSupervisedUser()) {
436 SetSupervisedUserTheme();
437 return;
439 #endif
440 ClearAllThemeData();
441 NotifyThemeChanged();
444 void ThemeService::UseSystemTheme() {
445 UseDefaultTheme();
448 bool ThemeService::UsingDefaultTheme() const {
449 std::string id = GetThemeID();
450 return id == ThemeService::kDefaultThemeID ||
451 id == kDefaultThemeGalleryID;
454 std::string ThemeService::GetThemeID() const {
455 return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
458 color_utils::HSL ThemeService::GetTint(int id) const {
459 DCHECK(CalledOnValidThread());
461 color_utils::HSL hsl;
462 if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl))
463 return hsl;
465 return ThemeProperties::GetDefaultTint(id);
468 void ThemeService::ClearAllThemeData() {
469 if (!ready_)
470 return;
472 SwapThemeSupplier(nullptr);
474 // Clear our image cache.
475 FreePlatformCaches();
477 profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
478 SaveThemeID(kDefaultThemeID);
480 // There should be no more infobars. This may not be the case because of
481 // http://crbug.com/62154
482 // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
483 // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
484 base::MessageLoop::current()->PostTask(FROM_HERE,
485 base::Bind(&ThemeService::RemoveUnusedThemes,
486 weak_ptr_factory_.GetWeakPtr(),
487 true));
490 void ThemeService::LoadThemePrefs() {
491 PrefService* prefs = profile_->GetPrefs();
493 std::string current_id = GetThemeID();
494 if (current_id == kDefaultThemeID) {
495 #if defined(ENABLE_SUPERVISED_USERS)
496 // Supervised users have a different default theme.
497 if (IsSupervisedUser()) {
498 SetSupervisedUserTheme();
499 set_ready();
500 return;
502 #endif
503 if (ShouldInitWithSystemTheme())
504 UseSystemTheme();
505 else
506 UseDefaultTheme();
507 set_ready();
508 return;
511 bool loaded_pack = false;
513 // If we don't have a file pack, we're updating from an old version.
514 base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
515 if (path != base::FilePath()) {
516 SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id));
517 loaded_pack = theme_supplier_.get() != nullptr;
520 if (loaded_pack) {
521 content::RecordAction(UserMetricsAction("Themes.Loaded"));
522 set_ready();
524 // Else: wait for the extension service to be ready so that the theme pack
525 // can be recreated from the extension.
528 void ThemeService::NotifyThemeChanged() {
529 if (!ready_)
530 return;
532 DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
533 // Redraw!
534 content::NotificationService* service =
535 content::NotificationService::current();
536 service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
537 content::Source<ThemeService>(this),
538 content::NotificationService::NoDetails());
539 #if defined(OS_MACOSX)
540 NotifyPlatformThemeChanged();
541 #endif // OS_MACOSX
543 // Notify sync that theme has changed.
544 if (theme_syncable_service_.get()) {
545 theme_syncable_service_->OnThemeChange();
549 #if defined(USE_AURA)
550 void ThemeService::FreePlatformCaches() {
551 // Views (Skia) has no platform image cache to clear.
553 #endif
555 void ThemeService::OnExtensionServiceReady() {
556 if (!ready_) {
557 // If the ThemeService is not ready yet, the custom theme data pack needs to
558 // be recreated from the extension.
559 MigrateTheme();
560 set_ready();
562 // Send notification in case anyone requested data and cached it when the
563 // theme service was not ready yet.
564 NotifyThemeChanged();
567 #if defined(ENABLE_EXTENSIONS)
568 theme_observer_.reset(new ThemeObserver(this));
569 #endif
571 registrar_.Add(this,
572 extensions::NOTIFICATION_EXTENSION_ENABLED,
573 content::Source<Profile>(profile_));
575 base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
576 base::Bind(&ThemeService::RemoveUnusedThemes,
577 weak_ptr_factory_.GetWeakPtr(),
578 false),
579 base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay));
582 void ThemeService::MigrateTheme() {
583 // TODO(erg): We need to pop up a dialog informing the user that their
584 // theme is being migrated.
585 ExtensionService* service =
586 extensions::ExtensionSystem::Get(profile_)->extension_service();
587 const Extension* extension =
588 service ? service->GetExtensionById(GetThemeID(), false) : nullptr;
589 if (extension) {
590 DLOG(ERROR) << "Migrating theme";
591 BuildFromExtension(extension);
592 content::RecordAction(UserMetricsAction("Themes.Migrated"));
593 } else {
594 DLOG(ERROR) << "Theme is mysteriously gone.";
595 ClearAllThemeData();
596 content::RecordAction(UserMetricsAction("Themes.Gone"));
600 void ThemeService::SwapThemeSupplier(
601 scoped_refptr<CustomThemeSupplier> theme_supplier) {
602 if (theme_supplier_.get())
603 theme_supplier_->StopUsingTheme();
604 theme_supplier_ = theme_supplier;
605 if (theme_supplier_.get())
606 theme_supplier_->StartUsingTheme();
609 void ThemeService::SavePackName(const base::FilePath& pack_path) {
610 profile_->GetPrefs()->SetFilePath(
611 prefs::kCurrentThemePackFilename, pack_path);
614 void ThemeService::SaveThemeID(const std::string& id) {
615 profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
618 void ThemeService::BuildFromExtension(const Extension* extension) {
619 scoped_refptr<BrowserThemePack> pack(
620 BrowserThemePack::BuildFromExtension(extension));
621 if (!pack.get()) {
622 // TODO(erg): We've failed to install the theme; perhaps we should tell the
623 // user? http://crbug.com/34780
624 LOG(ERROR) << "Could not load theme.";
625 return;
628 ExtensionService* service =
629 extensions::ExtensionSystem::Get(profile_)->extension_service();
630 if (!service)
631 return;
633 // Write the packed file to disk.
634 base::FilePath pack_path =
635 extension->path().Append(chrome::kThemePackFilename);
636 service->GetFileTaskRunner()->PostTask(
637 FROM_HERE,
638 base::Bind(&WritePackToDiskCallback, pack, pack_path));
640 SavePackName(pack_path);
641 SwapThemeSupplier(pack);
644 #if defined(ENABLE_SUPERVISED_USERS)
645 bool ThemeService::IsSupervisedUser() const {
646 return profile_->IsSupervised();
649 void ThemeService::SetSupervisedUserTheme() {
650 SetCustomDefaultTheme(new SupervisedUserTheme);
652 #endif
654 void ThemeService::OnInfobarDisplayed() {
655 number_of_infobars_++;
658 void ThemeService::OnInfobarDestroyed() {
659 number_of_infobars_--;
661 if (number_of_infobars_ == 0)
662 RemoveUnusedThemes(false);
665 ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
666 return theme_syncable_service_.get();