theme_service.cc revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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/prefs/pref_service.h"
10#include "base/sequenced_task_runner.h"
11#include "base/string_util.h"
12#include "base/utf_string_conversions.h"
13#include "chrome/browser/extensions/extension_service.h"
14#include "chrome/browser/extensions/extension_system.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/themes/browser_theme_pack.h"
17#include "chrome/browser/themes/theme_properties.h"
18#include "chrome/browser/themes/theme_syncable_service.h"
19#include "chrome/common/chrome_constants.h"
20#include "chrome/common/chrome_notification_types.h"
21#include "chrome/common/extensions/extension_manifest_constants.h"
22#include "chrome/common/pref_names.h"
23#include "content/public/browser/notification_service.h"
24#include "content/public/browser/user_metrics.h"
25#include "grit/theme_resources.h"
26#include "grit/ui_resources.h"
27#include "ui/base/layout.h"
28#include "ui/base/resource/resource_bundle.h"
29#include "ui/gfx/image/image_skia.h"
30
31#if defined(OS_WIN)
32#include "ui/base/win/shell.h"
33#endif
34
35#if defined(USE_AURA) && !defined(USE_ASH) && defined(OS_LINUX)
36#include "ui/linux_ui/linux_ui.h"
37#endif
38
39using content::BrowserThread;
40using content::UserMetricsAction;
41using extensions::Extension;
42using ui::ResourceBundle;
43
44typedef ThemeProperties Properties;
45
46// The default theme if we haven't installed a theme yet or if we've clicked
47// the "Use Classic" button.
48const char* ThemeService::kDefaultThemeID = "";
49
50namespace {
51
52// The default theme if we've gone to the theme gallery and installed the
53// "Default" theme. We have to detect this case specifically. (By the time we
54// realize we've installed the default theme, we already have an extension
55// unpacked on the filesystem.)
56const char* kDefaultThemeGalleryID = "hkacjpbfdknhflllbcmjibkdeoafencn";
57
58SkColor TintForUnderline(SkColor input) {
59  return SkColorSetA(input, SkColorGetA(input) / 3);
60}
61
62SkColor IncreaseLightness(SkColor color, double percent) {
63  color_utils::HSL result;
64  color_utils::SkColorToHSL(color, &result);
65  result.l += (1 - result.l) * percent;
66  return color_utils::HSLToSkColor(result, SkColorGetA(color));
67}
68
69// Writes the theme pack to disk on a separate thread.
70void WritePackToDiskCallback(BrowserThemePack* pack,
71                             const base::FilePath& path) {
72  if (!pack->WriteToDisk(path))
73    NOTREACHED() << "Could not write theme pack to disk";
74}
75
76}  // namespace
77
78ThemeService::ThemeService()
79    : rb_(ResourceBundle::GetSharedInstance()),
80      profile_(NULL),
81      ready_(false),
82      number_of_infobars_(0) {
83}
84
85ThemeService::~ThemeService() {
86  FreePlatformCaches();
87}
88
89void ThemeService::Init(Profile* profile) {
90  DCHECK(CalledOnValidThread());
91  profile_ = profile;
92
93  LoadThemePrefs();
94
95  if (!ready_) {
96    registrar_.Add(this,
97                   chrome::NOTIFICATION_EXTENSIONS_READY,
98                   content::Source<Profile>(profile_));
99  }
100
101  theme_syncable_service_.reset(new ThemeSyncableService(profile_, this));
102}
103
104gfx::Image ThemeService::GetImageNamed(int id) const {
105  DCHECK(CalledOnValidThread());
106
107  gfx::Image image;
108  if (theme_pack_.get())
109    image = theme_pack_->GetImageNamed(id);
110
111#if defined(USE_AURA) && !defined(USE_ASH) && defined(OS_LINUX)
112  const ui::LinuxUI* linux_ui = ui::LinuxUI::instance();
113  if (image.IsEmpty() && linux_ui)
114    image = linux_ui->GetThemeImageNamed(id);
115#endif
116
117  if (image.IsEmpty())
118    image = rb_.GetNativeImageNamed(id);
119
120  return image;
121}
122
123gfx::ImageSkia* ThemeService::GetImageSkiaNamed(int id) const {
124  gfx::Image image = GetImageNamed(id);
125  if (image.IsEmpty())
126    return NULL;
127  // TODO(pkotwicz): Remove this const cast.  The gfx::Image interface returns
128  // its images const. GetImageSkiaNamed() also should but has many callsites.
129  return const_cast<gfx::ImageSkia*>(image.ToImageSkia());
130}
131
132SkColor ThemeService::GetColor(int id) const {
133  DCHECK(CalledOnValidThread());
134
135  SkColor color;
136  if (theme_pack_.get() && theme_pack_->GetColor(id, &color))
137    return color;
138
139#if defined(USE_AURA) && !defined(USE_ASH) && defined(OS_LINUX)
140  const ui::LinuxUI* linux_ui = ui::LinuxUI::instance();
141  if (linux_ui && linux_ui->GetColor(id, &color))
142    return color;
143#endif
144
145  // For backward compat with older themes, some newer colors are generated from
146  // older ones if they are missing.
147  switch (id) {
148    case Properties::COLOR_NTP_SECTION_HEADER_TEXT:
149      return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.30);
150    case Properties::COLOR_NTP_SECTION_HEADER_TEXT_HOVER:
151      return GetColor(Properties::COLOR_NTP_TEXT);
152    case Properties::COLOR_NTP_SECTION_HEADER_RULE:
153      return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.70);
154    case Properties::COLOR_NTP_SECTION_HEADER_RULE_LIGHT:
155      return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.86);
156    case Properties::COLOR_NTP_TEXT_LIGHT:
157      return IncreaseLightness(GetColor(Properties::COLOR_NTP_TEXT), 0.40);
158  }
159
160  return Properties::GetDefaultColor(id);
161}
162
163bool ThemeService::GetDisplayProperty(int id, int* result) const {
164  if (theme_pack_.get())
165    return theme_pack_->GetDisplayProperty(id, result);
166
167  return Properties::GetDefaultDisplayProperty(id, result);
168}
169
170bool ThemeService::ShouldUseNativeFrame() const {
171  if (HasCustomImage(IDR_THEME_FRAME))
172    return false;
173#if defined(OS_WIN)
174  return ui::win::IsAeroGlassEnabled();
175#else
176  return false;
177#endif
178}
179
180bool ThemeService::HasCustomImage(int id) const {
181  if (!Properties::IsThemeableImage(id))
182    return false;
183
184  if (theme_pack_)
185    return theme_pack_->HasCustomImage(id);
186
187  return false;
188}
189
190base::RefCountedMemory* ThemeService::GetRawData(
191    int id,
192    ui::ScaleFactor scale_factor) const {
193  // Check to see whether we should substitute some images.
194  int ntp_alternate;
195  GetDisplayProperty(Properties::NTP_LOGO_ALTERNATE, &ntp_alternate);
196  if (id == IDR_PRODUCT_LOGO && ntp_alternate != 0)
197    id = IDR_PRODUCT_LOGO_WHITE;
198
199  base::RefCountedMemory* data = NULL;
200  if (theme_pack_.get())
201    data = theme_pack_->GetRawData(id, scale_factor);
202  if (!data)
203    data = rb_.LoadDataResourceBytesForScale(id, ui::SCALE_FACTOR_100P);
204
205  return data;
206}
207
208void ThemeService::Observe(int type,
209                           const content::NotificationSource& source,
210                           const content::NotificationDetails& details) {
211  DCHECK(type == chrome::NOTIFICATION_EXTENSIONS_READY);
212  registrar_.Remove(this, chrome::NOTIFICATION_EXTENSIONS_READY,
213      content::Source<Profile>(profile_));
214
215  MigrateTheme();
216  set_ready();
217
218  // Send notification in case anyone requested data and cached it when the
219  // theme service was not ready yet.
220  NotifyThemeChanged();
221}
222
223void ThemeService::SetTheme(const Extension* extension) {
224  // Clear our image cache.
225  FreePlatformCaches();
226
227  DCHECK(extension);
228  DCHECK(extension->is_theme());
229  if (DCHECK_IS_ON()) {
230    ExtensionService* service =
231        extensions::ExtensionSystem::Get(profile_)->extension_service();
232    DCHECK(service);
233    DCHECK(service->GetExtensionById(extension->id(), false));
234  }
235
236  BuildFromExtension(extension);
237  SaveThemeID(extension->id());
238
239  NotifyThemeChanged();
240  content::RecordAction(UserMetricsAction("Themes_Installed"));
241}
242
243void ThemeService::RemoveUnusedThemes() {
244  if (!profile_)
245    return;
246  ExtensionService* service = profile_->GetExtensionService();
247  if (!service)
248    return;
249  std::string current_theme = GetThemeID();
250  std::vector<std::string> remove_list;
251  const ExtensionSet* extensions = service->extensions();
252  for (ExtensionSet::const_iterator it = extensions->begin();
253       it != extensions->end(); ++it) {
254    if ((*it)->is_theme() && (*it)->id() != current_theme) {
255      remove_list.push_back((*it)->id());
256    }
257  }
258  for (size_t i = 0; i < remove_list.size(); ++i)
259    service->UninstallExtension(remove_list[i], false, NULL);
260}
261
262void ThemeService::UseDefaultTheme() {
263  ClearAllThemeData();
264  NotifyThemeChanged();
265  content::RecordAction(UserMetricsAction("Themes_Reset"));
266}
267
268void ThemeService::SetNativeTheme() {
269  UseDefaultTheme();
270}
271
272bool ThemeService::UsingDefaultTheme() const {
273  std::string id = GetThemeID();
274  return id == ThemeService::kDefaultThemeID ||
275      id == kDefaultThemeGalleryID;
276}
277
278bool ThemeService::UsingNativeTheme() const {
279  return UsingDefaultTheme();
280}
281
282std::string ThemeService::GetThemeID() const {
283  return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
284}
285
286color_utils::HSL ThemeService::GetTint(int id) const {
287  DCHECK(CalledOnValidThread());
288
289  color_utils::HSL hsl;
290  if (theme_pack_.get() && theme_pack_->GetTint(id, &hsl))
291    return hsl;
292
293  return ThemeProperties::GetDefaultTint(id);
294}
295
296void ThemeService::ClearAllThemeData() {
297  // Clear our image cache.
298  FreePlatformCaches();
299  theme_pack_ = NULL;
300
301  profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
302  SaveThemeID(kDefaultThemeID);
303
304  RemoveUnusedThemes();
305}
306
307void ThemeService::LoadThemePrefs() {
308  PrefService* prefs = profile_->GetPrefs();
309
310  std::string current_id = GetThemeID();
311  if (current_id == kDefaultThemeID) {
312    set_ready();
313    return;
314  }
315
316  bool loaded_pack = false;
317
318  // If we don't have a file pack, we're updating from an old version.
319  base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
320  if (path != base::FilePath()) {
321    theme_pack_ = BrowserThemePack::BuildFromDataPack(path, current_id);
322    loaded_pack = theme_pack_.get() != NULL;
323  }
324
325  if (loaded_pack) {
326    content::RecordAction(UserMetricsAction("Themes.Loaded"));
327    set_ready();
328  } else {
329    // TODO(erg): We need to pop up a dialog informing the user that their
330    // theme is being migrated.
331    ExtensionService* service =
332        extensions::ExtensionSystem::Get(profile_)->extension_service();
333    if (service && service->is_ready()) {
334      MigrateTheme();
335      set_ready();
336    }
337  }
338}
339
340void ThemeService::NotifyThemeChanged() {
341  DVLOG(1) << "Sending BROWSER_THEME_CHANGED";
342  // Redraw!
343  content::NotificationService* service =
344      content::NotificationService::current();
345  service->Notify(chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
346                  content::Source<ThemeService>(this),
347                  content::NotificationService::NoDetails());
348#if defined(OS_MACOSX)
349  NotifyPlatformThemeChanged();
350#endif  // OS_MACOSX
351
352  // Notify sync that theme has changed.
353  if (theme_syncable_service_.get()) {
354    theme_syncable_service_->OnThemeChange();
355  }
356}
357
358#if defined(OS_WIN) || defined(USE_AURA)
359void ThemeService::FreePlatformCaches() {
360  // Views (Skia) has no platform image cache to clear.
361}
362#endif
363
364void ThemeService::MigrateTheme() {
365  ExtensionService* service =
366      extensions::ExtensionSystem::Get(profile_)->extension_service();
367  const Extension* extension = service ?
368      service->GetExtensionById(GetThemeID(), false) : NULL;
369  if (extension) {
370    DLOG(ERROR) << "Migrating theme";
371    BuildFromExtension(extension);
372    content::RecordAction(UserMetricsAction("Themes.Migrated"));
373  } else {
374    DLOG(ERROR) << "Theme is mysteriously gone.";
375    ClearAllThemeData();
376    content::RecordAction(UserMetricsAction("Themes.Gone"));
377  }
378}
379
380void ThemeService::SavePackName(const base::FilePath& pack_path) {
381  profile_->GetPrefs()->SetFilePath(
382      prefs::kCurrentThemePackFilename, pack_path);
383}
384
385void ThemeService::SaveThemeID(const std::string& id) {
386  profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
387}
388
389void ThemeService::BuildFromExtension(const Extension* extension) {
390  scoped_refptr<BrowserThemePack> pack(
391      BrowserThemePack::BuildFromExtension(extension));
392  if (!pack.get()) {
393    // TODO(erg): We've failed to install the theme; perhaps we should tell the
394    // user? http://crbug.com/34780
395    LOG(ERROR) << "Could not load theme.";
396    return;
397  }
398
399  ExtensionService* service =
400      extensions::ExtensionSystem::Get(profile_)->extension_service();
401  if (!service)
402    return;
403
404  // Write the packed file to disk.
405  base::FilePath pack_path =
406      extension->path().Append(chrome::kThemePackFilename);
407  service->GetFileTaskRunner()->PostTask(
408      FROM_HERE,
409      base::Bind(&WritePackToDiskCallback, pack, pack_path));
410
411  SavePackName(pack_path);
412  theme_pack_ = pack;
413}
414
415void ThemeService::OnInfobarDisplayed() {
416  number_of_infobars_++;
417}
418
419void ThemeService::OnInfobarDestroyed() {
420  number_of_infobars_--;
421
422  if (number_of_infobars_ == 0)
423    RemoveUnusedThemes();
424}
425
426ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
427  return theme_syncable_service_.get();
428}
429