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