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