Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / themes / theme_service.cc
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.
4
5 #include "chrome/browser/themes/theme_service.h"
6
7 #include <algorithm>
8
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/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 "ui/base/layout.h"
36 #include "ui/base/resource/resource_bundle.h"
37 #include "ui/gfx/image/image_skia.h"
38
39 #if defined(OS_WIN)
40 #include "ui/base/win/shell.h"
41 #endif
42
43 using base::UserMetricsAction;
44 using content::BrowserThread;
45 using extensions::Extension;
46 using extensions::UnloadedExtensionInfo;
47 using ui::ResourceBundle;
48
49 typedef ThemeProperties Properties;
50
51 // The default theme if we haven't installed a theme yet or if we've clicked
52 // the "Use Classic" button.
53 const char* ThemeService::kDefaultThemeID = "";
54
55 namespace {
56
57 // The default theme if we've gone to the theme gallery and installed the
58 // "Default" theme. We have to detect this case specifically. (By the time we
59 // realize we've installed the default theme, we already have an extension
60 // unpacked on the filesystem.)
61 const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn";
62
63 // Wait this many seconds after startup to garbage collect unused themes.
64 // Removing unused themes is done after a delay because there is no
65 // reason to do it at startup.
66 // ExtensionService::GarbageCollectExtensions() does something similar.
67 const int kRemoveUnusedThemesStartupDelay = 30;
68
69 SkColor IncreaseLightness(SkColor color, double percent) {
70   color_utils::HSL result;
71   color_utils::SkColorToHSL(color, &result);
72   result.l += (1 - result.l) * percent;
73   return color_utils::HSLToSkColor(result, SkColorGetA(color));
74 }
75
76 // Writes the theme pack to disk on a separate thread.
77 void WritePackToDiskCallback(BrowserThemePack* pack,
78                              const base::FilePath& path) {
79   if (!pack->WriteToDisk(path))
80     NOTREACHED() << "Could not write theme pack to disk";
81 }
82
83 // Heuristic to determine if color is grayscale. This is used to decide whether
84 // to use the colorful or white logo, if a theme fails to specify which.
85 bool IsColorGrayscale(SkColor color) {
86   const int kChannelTolerance = 9;
87   int r = SkColorGetR(color);
88   int g = SkColorGetG(color);
89   int b = SkColorGetB(color);
90   int range = std::max(r, std::max(g, b)) - std::min(r, std::min(g, b));
91   return range < kChannelTolerance;
92 }
93
94 }  // namespace
95
96 ThemeService::ThemeService()
97     : ready_(false),
98       rb_(ResourceBundle::GetSharedInstance()),
99       profile_(NULL),
100       installed_pending_load_id_(kDefaultThemeID),
101       number_of_infobars_(0),
102       weak_ptr_factory_(this) {
103 }
104
105 ThemeService::~ThemeService() {
106   FreePlatformCaches();
107 }
108
109 void ThemeService::Init(Profile* profile) {
110   DCHECK(CalledOnValidThread());
111   profile_ = profile;
112
113   LoadThemePrefs();
114
115   registrar_.Add(this,
116                  extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
117                  content::Source<Profile>(profile_));
118
119   theme_syncable_service_.reset(new ThemeSyncableService(profile_, this));
120 }
121
122 gfx::Image ThemeService::GetImageNamed(int id) const {
123   DCHECK(CalledOnValidThread());
124
125   gfx::Image image;
126   if (theme_supplier_.get())
127     image = theme_supplier_->GetImageNamed(id);
128
129   if (image.IsEmpty())
130     image = rb_.GetNativeImageNamed(id);
131
132   return image;
133 }
134
135 bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
136   return false;
137 }
138
139 bool ThemeService::UsingSystemTheme() const {
140   return UsingDefaultTheme();
141 }
142
143 gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const {
144   gfx::Image image = GetImageNamed(id);
145   if (image.IsEmpty())
146     return NULL;
147   // TODO(pkotwicz): Remove this const cast.  The gfx::Image interface returns
148   // its images const. GetImageSkiaNamed() also should but has many callsites.
149   return const_cast<gfx::ImageSkia*>(image.ToImageSkia());
150 }
151
152 SkColor ThemeService::GetColor(int id) const {
153   DCHECK(CalledOnValidThread());
154   SkColor color;
155   if (theme_supplier_.get() && theme_supplier_->GetColor(id, &color))
156     return color;
157
158   // For backward compat with older themes, some newer colors are generated from
159   // older ones if they are missing.
160   switch (id) {
161     case Properties::COLOR_NTP_SECTION_HEADER_TEXT:
162       return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30);
163     case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER:
164       return GetColor(Properties::COLOR_NTP_TEXT);
165     case Properties::COLOR_NTP_SECTION_HEADER_RULE:
166       return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70);
167     case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT:
168       return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86);
169     case Properties::COLOR_NTP_TEXT_LIGHT:
170       return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40);
171     case Properties::COLOR_SUPERVISED_USER_LABEL:
172       return color_utils::GetReadableColor(
173           SK_ColorWHITE,
174           GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND));
175     case Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND:
176       return color_utils::BlendTowardOppositeLuminance(
177           GetColor(Properties::COLOR_FRAME), 0x80);
178     case Properties::COLOR_SUPERVISED_USER_LABEL_BORDER:
179       return color_utils::AlphaBlend(
180           GetColor(Properties::COLOR_SUPERVISED_USER_LABEL_BACKGROUND),
181           SK_ColorBLACK,
182           230);
183     case Properties::COLOR_STATUS_BAR_TEXT: {
184       // A long time ago, we blended the toolbar and the tab text together to
185       // get the status bar text because, at the time, our text rendering in
186       // views couldn't do alpha blending. Even though this is no longer the
187       // case, this blending decision is built into the majority of themes that
188       // exist, and we must keep doing it.
189       SkColor toolbar_color = GetColor(Properties::COLOR_TOOLBAR);
190       SkColor text_color = GetColor(Properties::COLOR_TAB_TEXT);
191       return SkColorSetARGB(
192           SkColorGetA(text_color),
193           (SkColorGetR(text_color) + SkColorGetR(toolbar_color)) / 2,
194           (SkColorGetG(text_color) + SkColorGetR(toolbar_color)) / 2,
195           (SkColorGetB(text_color) + SkColorGetR(toolbar_color)) / 2);
196     }
197   }
198
199   return Properties::GetDefaultColor(id);
200 }
201
202 int ThemeService::GetDisplayProperty(int id) const {
203   int result = 0;
204   if (theme_supplier_.get() &&
205       theme_supplier_->GetDisplayProperty(id, &result)) {
206     return result;
207   }
208
209   if (id == Properties::NTP_LOGO_ALTERNATE) {
210     if (UsingDefaultTheme() || UsingSystemTheme())
211       return 0;  // Colorful logo.
212
213     if (HasCustomImage(IDR_THEME_NTP_BACKGROUND))
214       return 1;  // White logo.
215
216     SkColor background_color = GetColor(Properties::COLOR_NTP_BACKGROUND);
217     return IsColorGrayscale(background_color) ? 0 : 1;
218   }
219
220   return Properties::GetDefaultDisplayProperty(id);
221 }
222
223 bool ThemeService::ShouldUseNativeFrame() const {
224   if (HasCustomImage(IDR_THEME_FRAME))
225     return false;
226 #if defined(OS_WIN)
227   return ui::win::IsAeroGlassEnabled();
228 #else
229   return false;
230 #endif
231 }
232
233 bool ThemeService::HasCustomImage(int id) const {
234   if (!Properties::IsThemeableImage(id))
235     return false;
236
237   if (theme_supplier_.get())
238     return theme_supplier_->HasCustomImage(id);
239
240   return false;
241 }
242
243 base::RefCountedMemory* ThemeService::GetRawData(
244     int id,
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;
250
251   base::RefCountedMemory* data = NULL;
252   if (theme_supplier_.get())
253     data = theme_supplier_->GetRawData(id, scale_factor);
254   if (!data)
255     data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P);
256
257   return data;
258 }
259
260 void ThemeService::Observe(int type,
261                            const content::NotificationSource& source,
262                            const content::NotificationDetails& details) {
263   using content::Details;
264   switch (type) {
265     case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED:
266       registrar_.Remove(this,
267                         extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
268                         content::Source<Profile>(profile_));
269       OnExtensionServiceReady();
270       break;
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(
274           details);
275       if (installed_details->extension->is_theme())
276         installed_pending_load_id_ = installed_details->extension->id();
277       break;
278     }
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()) {
284         SetTheme(extension);
285       }
286       installed_pending_load_id_ = kDefaultThemeID;
287       break;
288     }
289     case extensions::NOTIFICATION_EXTENSION_ENABLED: {
290       const Extension* extension = Details<const Extension>(details).ptr();
291       if (extension->is_theme())
292         SetTheme(extension);
293       break;
294     }
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->extension->is_theme() &&
299           unloaded_details->extension->id() == GetThemeID()) {
300         UseDefaultTheme();
301       }
302       break;
303     }
304   }
305 }
306
307 void ThemeService::SetTheme(const Extension* extension) {
308   DCHECK(extension->is_theme());
309   ExtensionService* service =
310       extensions::ExtensionSystem::Get(profile_)->extension_service();
311   if (!service->IsExtensionEnabled(extension->id())) {
312     // |extension| is disabled when reverting to the previous theme via an
313     // infobar.
314     service->EnableExtension(extension->id());
315     // Enabling the extension will call back to SetTheme().
316     return;
317   }
318
319   std::string previous_theme_id = GetThemeID();
320
321   // Clear our image cache.
322   FreePlatformCaches();
323
324   BuildFromExtension(extension);
325   SaveThemeID(extension->id());
326
327   NotifyThemeChanged();
328   content::RecordAction(UserMetricsAction("Themes_Installed"));
329
330   if (previous_theme_id != kDefaultThemeID &&
331       previous_theme_id != extension->id()) {
332     // Disable the old theme.
333     service->DisableExtension(previous_theme_id,
334                               extensions::Extension::DISABLE_USER_ACTION);
335   }
336 }
337
338 void ThemeService::SetCustomDefaultTheme(
339     scoped_refptr<CustomThemeSupplier> theme_supplier) {
340   ClearAllThemeData();
341   SwapThemeSupplier(theme_supplier);
342   NotifyThemeChanged();
343 }
344
345 bool ThemeService::ShouldInitWithSystemTheme() const {
346   return false;
347 }
348
349 void ThemeService::RemoveUnusedThemes(bool ignore_infobars) {
350   // We do not want to garbage collect themes on startup (|ready_| is false).
351   // Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
352   if (!profile_ || !ready_)
353     return;
354   if (!ignore_infobars && number_of_infobars_ != 0)
355     return;
356
357   ExtensionService* service =
358       extensions::ExtensionSystem::Get(profile_)->extension_service();
359   if (!service)
360     return;
361
362   std::string current_theme = GetThemeID();
363   std::vector<std::string> remove_list;
364   scoped_ptr<const extensions::ExtensionSet> extensions(
365       extensions::ExtensionRegistry::Get(profile_)
366           ->GenerateInstalledExtensionsSet());
367   extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
368   for (extensions::ExtensionSet::const_iterator it = extensions->begin();
369        it != extensions->end(); ++it) {
370     const extensions::Extension* extension = it->get();
371     if (extension->is_theme() &&
372         extension->id() != current_theme) {
373       // Only uninstall themes which are not disabled or are disabled with
374       // reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
375       // themes because externally installed themes are initially disabled.
376       int disable_reason = prefs->GetDisableReasons(extension->id());
377       if (!prefs->IsExtensionDisabled(extension->id()) ||
378           disable_reason == Extension::DISABLE_USER_ACTION) {
379         remove_list.push_back((*it)->id());
380       }
381     }
382   }
383   // TODO: Garbage collect all unused themes. This method misses themes which
384   // are installed but not loaded because they are blacklisted by a management
385   // policy provider.
386
387   for (size_t i = 0; i < remove_list.size(); ++i) {
388     service->UninstallExtension(remove_list[i],
389                                 extensions::UNINSTALL_REASON_ORPHANED_THEME,
390                                 base::Bind(&base::DoNothing),
391                                 NULL);
392   }
393 }
394
395 void ThemeService::UseDefaultTheme() {
396   if (ready_)
397     content::RecordAction(UserMetricsAction("Themes_Reset"));
398   if (IsSupervisedUser()) {
399     SetSupervisedUserTheme();
400     return;
401   }
402   ClearAllThemeData();
403   NotifyThemeChanged();
404 }
405
406 void ThemeService::UseSystemTheme() {
407   UseDefaultTheme();
408 }
409
410 bool ThemeService::UsingDefaultTheme() const {
411   std::string id = GetThemeID();
412   return id == ThemeService::kDefaultThemeID ||
413       id == kDefaultThemeGalleryID;
414 }
415
416 std::string ThemeService::GetThemeID() const {
417   return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
418 }
419
420 color_utils::HSL ThemeService::GetTint(int id) const {
421   DCHECK(CalledOnValidThread());
422
423   color_utils::HSL hsl;
424   if (theme_supplier_.get() && theme_supplier_->GetTint(id, &hsl))
425     return hsl;
426
427   return ThemeProperties::GetDefaultTint(id);
428 }
429
430 void ThemeService::ClearAllThemeData() {
431   if (!ready_)
432     return;
433
434   SwapThemeSupplier(NULL);
435
436   // Clear our image cache.
437   FreePlatformCaches();
438
439   profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
440   SaveThemeID(kDefaultThemeID);
441
442   // There should be no more infobars. This may not be the case because of
443   // http://crbug.com/62154
444   // RemoveUnusedThemes is called on a task because ClearAllThemeData() may
445   // be called as a result of NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED.
446   base::MessageLoop::current()->PostTask(FROM_HERE,
447       base::Bind(&ThemeService::RemoveUnusedThemes,
448                  weak_ptr_factory_.GetWeakPtr(),
449                  true));
450 }
451
452 void ThemeService::LoadThemePrefs() {
453   PrefService* prefs = profile_->GetPrefs();
454
455   std::string current_id = GetThemeID();
456   if (current_id == kDefaultThemeID) {
457     // Supervised users have a different default theme.
458     if (IsSupervisedUser())
459       SetSupervisedUserTheme();
460     else if (ShouldInitWithSystemTheme())
461       UseSystemTheme();
462     else
463       UseDefaultTheme();
464     set_ready();
465     return;
466   }
467
468   bool loaded_pack = false;
469
470   // If we don't have a file pack, we're updating from an old version.
471   base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
472   if (path != base::FilePath()) {
473     SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id));
474     loaded_pack = theme_supplier_.get() != NULL;
475   }
476
477   if (loaded_pack) {
478     content::RecordAction(UserMetricsAction("Themes.Loaded"));
479     set_ready();
480   }
481   // Else: wait for the extension service to be ready so that the theme pack
482   // can be recreated from the extension.
483 }
484
485 void ThemeService::NotifyThemeChanged() {
486   if (!ready_)
487     return;
488
489   DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
490   // Redraw!
491   content::NotificationService* service =
492       content::NotificationService::current();
493   service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
494                   content::Source<ThemeService>(this),
495                   content::NotificationService::NoDetails());
496 #if defined(OS_MACOSX)
497   NotifyPlatformThemeChanged();
498 #endif  // OS_MACOSX
499
500   // Notify sync that theme has changed.
501   if (theme_syncable_service_.get()) {
502     theme_syncable_service_->OnThemeChange();
503   }
504 }
505
506 #if defined(USE_AURA)
507 void ThemeService::FreePlatformCaches() {
508   // Views (Skia) has no platform image cache to clear.
509 }
510 #endif
511
512 void ThemeService::OnExtensionServiceReady() {
513   if (!ready_) {
514     // If the ThemeService is not ready yet, the custom theme data pack needs to
515     // be recreated from the extension.
516     MigrateTheme();
517     set_ready();
518
519     // Send notification in case anyone requested data and cached it when the
520     // theme service was not ready yet.
521     NotifyThemeChanged();
522   }
523
524   registrar_.Add(
525       this,
526       extensions::NOTIFICATION_EXTENSION_WILL_BE_INSTALLED_DEPRECATED,
527       content::Source<Profile>(profile_));
528   registrar_.Add(this,
529                  extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
530                  content::Source<Profile>(profile_));
531   registrar_.Add(this,
532                  extensions::NOTIFICATION_EXTENSION_ENABLED,
533                  content::Source<Profile>(profile_));
534   registrar_.Add(this,
535                  extensions::NOTIFICATION_EXTENSION_UNLOADED_DEPRECATED,
536                  content::Source<Profile>(profile_));
537
538   base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
539       base::Bind(&ThemeService::RemoveUnusedThemes,
540                  weak_ptr_factory_.GetWeakPtr(),
541                  false),
542       base::TimeDelta::FromSeconds(kRemoveUnusedThemesStartupDelay));
543 }
544
545 void ThemeService::MigrateTheme() {
546   // TODO(erg): We need to pop up a dialog informing the user that their
547   // theme is being migrated.
548   ExtensionService* service =
549       extensions::ExtensionSystem::Get(profile_)->extension_service();
550   const Extension* extension = service ?
551       service->GetExtensionById(GetThemeID(), false) : NULL;
552   if (extension) {
553     DLOG(ERROR) << "Migrating theme";
554     BuildFromExtension(extension);
555     content::RecordAction(UserMetricsAction("Themes.Migrated"));
556   } else {
557     DLOG(ERROR) << "Theme is mysteriously gone.";
558     ClearAllThemeData();
559     content::RecordAction(UserMetricsAction("Themes.Gone"));
560   }
561 }
562
563 void ThemeService::SwapThemeSupplier(
564     scoped_refptr<CustomThemeSupplier> theme_supplier) {
565   if (theme_supplier_.get())
566     theme_supplier_->StopUsingTheme();
567   theme_supplier_ = theme_supplier;
568   if (theme_supplier_.get())
569     theme_supplier_->StartUsingTheme();
570 }
571
572 void ThemeService::SavePackName(const base::FilePath& pack_path) {
573   profile_->GetPrefs()->SetFilePath(
574       prefs::kCurrentThemePackFilename, pack_path);
575 }
576
577 void ThemeService::SaveThemeID(const std::string& id) {
578   profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
579 }
580
581 void ThemeService::BuildFromExtension(const Extension* extension) {
582   scoped_refptr<BrowserThemePack> pack(
583       BrowserThemePack::BuildFromExtension(extension));
584   if (!pack.get()) {
585     // TODO(erg): We've failed to install the theme; perhaps we should tell the
586     // user? http://crbug.com/34780
587     LOG(ERROR) << "Could not load theme.";
588     return;
589   }
590
591   ExtensionService* service =
592       extensions::ExtensionSystem::Get(profile_)->extension_service();
593   if (!service)
594     return;
595
596   // Write the packed file to disk.
597   base::FilePath pack_path =
598       extension->path().Append(chrome::kThemePackFilename);
599   service->GetFileTaskRunner()->PostTask(
600       FROM_HERE,
601       base::Bind(&WritePackToDiskCallback, pack, pack_path));
602
603   SavePackName(pack_path);
604   SwapThemeSupplier(pack);
605 }
606
607 bool ThemeService::IsSupervisedUser() const {
608   return profile_->IsSupervised();
609 }
610
611 void ThemeService::SetSupervisedUserTheme() {
612   SetCustomDefaultTheme(new SupervisedUserTheme);
613 }
614
615 void ThemeService::OnInfobarDisplayed() {
616   number_of_infobars_++;
617 }
618
619 void ThemeService::OnInfobarDestroyed() {
620   number_of_infobars_--;
621
622   if (number_of_infobars_ == 0)
623     RemoveUnusedThemes(false);
624 }
625
626 ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
627   return theme_syncable_service_.get();
628 }