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