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
37using content::BrowserThread;
38using content::UserMetricsAction;
39using extensions::Extension;
40using extensions::UnloadedExtensionInfo;
41using ui::ResourceBundle;
42
43typedef 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.
47const char* ThemeService::kDefaultThemeID = "";
48
49namespace {
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.)
55const 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.
61const int kRemoveUnusedThemesStartupDelay = 30;
62
63SkColor 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.
71void 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
79ThemeService::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
88ThemeService::~ThemeService() {
89  FreePlatformCaches();
90}
91
92void 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
105gfx::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
118gfx::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
127SkColor 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
177int 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
195bool 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
205bool 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
215base::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
232void 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
282void 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
313void ThemeService::SetCustomDefaultTheme(
314    scoped_refptr<CustomThemeSupplier> theme_supplier) {
315  ClearAllThemeData();
316  SwapThemeSupplier(theme_supplier);
317  NotifyThemeChanged();
318}
319
320bool ThemeService::ShouldInitWithNativeTheme() const {
321  return false;
322}
323
324void 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
363void 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
374void ThemeService::SetNativeTheme() {
375  UseDefaultTheme();
376}
377
378bool ThemeService::UsingDefaultTheme() const {
379  std::string id = GetThemeID();
380  return id == ThemeService::kDefaultThemeID ||
381      id == kDefaultThemeGalleryID;
382}
383
384bool ThemeService::UsingNativeTheme() const {
385  return UsingDefaultTheme();
386}
387
388std::string ThemeService::GetThemeID() const {
389  return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
390}
391
392color_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
402void 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
424void 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
457void 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)
479void ThemeService::FreePlatformCaches() {
480  // Views (Skia) has no platform image cache to clear.
481}
482#endif
483
484void 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
516void 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
534void 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
543void ThemeService::SavePackName(const base::FilePath& pack_path) {
544  profile_->GetPrefs()->SetFilePath(
545      prefs::kCurrentThemePackFilename, pack_path);
546}
547
548void ThemeService::SaveThemeID(const std::string& id) {
549  profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
550}
551
552void 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
578bool ThemeService::IsManagedUser() const {
579  return profile_->IsManaged();
580}
581
582void ThemeService::SetManagedUserTheme() {
583  SetCustomDefaultTheme(new ManagedUserTheme);
584}
585
586void ThemeService::OnInfobarDisplayed() {
587  number_of_infobars_++;
588}
589
590void ThemeService::OnInfobarDestroyed() {
591  number_of_infobars_--;
592
593  if (number_of_infobars_ == 0)
594    RemoveUnusedThemes(false);
595}
596
597ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
598  return theme_syncable_service_.get();
599}
600