Home | History | Annotate | Download | only in themes
      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 
     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.
     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 
     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 
     88 ThemeService::~ThemeService() {
     89   FreePlatformCaches();
     90 }
     91 
     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 
    105 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 
    118 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 
    127 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 
    177 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 
    195 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 
    205 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 
    215 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 
    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 
    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 
    313 void ThemeService::SetCustomDefaultTheme(
    314     scoped_refptr<CustomThemeSupplier> theme_supplier) {
    315   ClearAllThemeData();
    316   SwapThemeSupplier(theme_supplier);
    317   NotifyThemeChanged();
    318 }
    319 
    320 bool ThemeService::ShouldInitWithNativeTheme() const {
    321   return false;
    322 }
    323 
    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 
    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 
    374 void ThemeService::SetNativeTheme() {
    375   UseDefaultTheme();
    376 }
    377 
    378 bool ThemeService::UsingDefaultTheme() const {
    379   std::string id = GetThemeID();
    380   return id == ThemeService::kDefaultThemeID ||
    381       id == kDefaultThemeGalleryID;
    382 }
    383 
    384 bool ThemeService::UsingNativeTheme() const {
    385   return UsingDefaultTheme();
    386 }
    387 
    388 std::string ThemeService::GetThemeID() const {
    389   return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
    390 }
    391 
    392 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 
    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 
    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 
    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)
    479 void ThemeService::FreePlatformCaches() {
    480   // Views (Skia) has no platform image cache to clear.
    481 }
    482 #endif
    483 
    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 
    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 
    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 
    543 void ThemeService::SavePackName(const base::FilePath& pack_path) {
    544   profile_->GetPrefs()->SetFilePath(
    545       prefs::kCurrentThemePackFilename, pack_path);
    546 }
    547 
    548 void ThemeService::SaveThemeID(const std::string& id) {
    549   profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, id);
    550 }
    551 
    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 
    578 bool ThemeService::IsManagedUser() const {
    579   return profile_->IsManaged();
    580 }
    581 
    582 void ThemeService::SetManagedUserTheme() {
    583   SetCustomDefaultTheme(new ManagedUserTheme);
    584 }
    585 
    586 void ThemeService::OnInfobarDisplayed() {
    587   number_of_infobars_++;
    588 }
    589 
    590 void ThemeService::OnInfobarDestroyed() {
    591   number_of_infobars_--;
    592 
    593   if (number_of_infobars_ == 0)
    594     RemoveUnusedThemes(false);
    595 }
    596 
    597 ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
    598   return theme_syncable_service_.get();
    599 }
    600