blob: bd121a5a2483e0cd8992d0e8da6a63cb70010df2 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/themes/theme_service.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <optional>
#include "base/command_line.h"
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/observer_list.h"
#include "base/one_shot_event.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/theme_installed_infobar_delegate.h"
#include "chrome/browser/new_tab_page/chrome_colors/chrome_colors_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/background/ntp_custom_background_service.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/custom_theme_supplier.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/themes/theme_service_observer.h"
#include "chrome/browser/themes/theme_syncable_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_set.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "ui/base/mojom/themes.mojom.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/native_theme/native_theme.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/scoped_observation.h"
#include "extensions/browser/extension_registry_observer.h"
#endif
#if BUILDFLAG(IS_LINUX)
#include "ui/linux/linux_ui.h"
#include "ui/ozone/public/ozone_platform.h"
#endif
using TP = ThemeProperties;
// Helpers --------------------------------------------------------------------
namespace {
// Wait this many seconds after startup to garbage collect unused themes.
// Removing unused themes is done after a delay because there is no
// reason to do it at startup.
// ExtensionService::GarbageCollectExtensions() does something similar.
constexpr base::TimeDelta kRemoveUnusedThemesStartupDelay = base::Seconds(30);
bool g_dont_write_theme_pack_for_testing = false;
// Writes the theme pack to disk on a separate thread.
void WritePackToDiskCallback(BrowserThemePack* pack,
const base::FilePath& directory) {
if (g_dont_write_theme_pack_for_testing)
return;
pack->WriteToDisk(directory.Append(chrome::kThemePackFilename));
}
} // namespace
// ThemeService::ThemeObserver ------------------------------------------------
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ThemeService::ThemeObserver
: public extensions::ExtensionRegistryObserver {
public:
explicit ThemeObserver(ThemeService* service) : theme_service_(service) {
extension_registry_observation_.Observe(
extensions::ExtensionRegistry::Get(theme_service_->profile_));
}
ThemeObserver(const ThemeObserver&) = delete;
ThemeObserver& operator=(const ThemeObserver&) = delete;
~ThemeObserver() override {
}
private:
// extensions::ExtensionRegistryObserver:
void OnExtensionWillBeInstalled(content::BrowserContext* browser_context,
const extensions::Extension* extension,
bool is_update,
const std::string& old_name) override {
if (extension->is_theme()) {
// Remember ID of the newly installed theme.
theme_service_->installed_pending_load_id_ = extension->id();
}
}
void OnExtensionLoaded(content::BrowserContext* browser_context,
const extensions::Extension* extension) override {
if (!extension->is_theme() || theme_service_->UsingPolicyTheme())
return;
bool is_new_version =
theme_service_->installed_pending_load_id_ !=
ThemeHelper::kDefaultThemeID &&
theme_service_->installed_pending_load_id_ == extension->id();
theme_service_->installed_pending_load_id_ = ThemeHelper::kDefaultThemeID;
// Do not load already loaded theme.
if (!is_new_version && extension->id() == theme_service_->GetThemeID())
return;
// Set the new theme during extension load:
// This includes: a) installing a new theme, b) enabling a disabled theme.
// We shouldn't get here for the update of a disabled theme.
theme_service_->DoSetTheme(extension, !is_new_version);
}
void OnExtensionUnloaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UnloadedExtensionReason reason) override {
if (reason != extensions::UnloadedExtensionReason::UPDATE &&
reason != extensions::UnloadedExtensionReason::LOCK_ALL &&
extension->is_theme() &&
extension->id() == theme_service_->GetThemeID()) {
theme_service_->UseDefaultTheme();
}
}
raw_ptr<ThemeService> theme_service_;
base::ScopedObservation<extensions::ExtensionRegistry,
extensions::ExtensionRegistryObserver>
extension_registry_observation_{this};
};
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
// ThemeService::ThemeReinstaller -----------------------------------------
ThemeService::ThemeReinstaller::ThemeReinstaller(Profile* profile,
base::OnceClosure installer)
: theme_service_(ThemeServiceFactory::GetForProfile(profile)) {
theme_service_->number_of_reinstallers_++;
installer_ = std::move(installer);
}
ThemeService::ThemeReinstaller::~ThemeReinstaller() {
theme_service_->number_of_reinstallers_--;
theme_service_->RemoveUnusedThemes();
}
void ThemeService::ThemeReinstaller::Reinstall() {
if (!installer_.is_null()) {
std::move(installer_).Run();
}
}
// ThemeService::BrowserThemeProvider ------------------------------------------
ThemeService::BrowserThemeProvider::BrowserThemeProvider(
const ThemeHelper& theme_helper,
bool incognito,
const BrowserThemeProviderDelegate* delegate)
: theme_helper_(theme_helper), incognito_(incognito), delegate_(delegate) {
DCHECK(delegate_);
}
ThemeService::BrowserThemeProvider::~BrowserThemeProvider() = default;
gfx::ImageSkia* ThemeService::BrowserThemeProvider::GetImageSkiaNamed(
int id) const {
return theme_helper_->GetImageSkiaNamed(id, incognito_, GetThemeSupplier());
}
color_utils::HSL ThemeService::BrowserThemeProvider::GetTint(int id) const {
return theme_helper_->GetTint(id, incognito_, GetThemeSupplier());
}
int ThemeService::BrowserThemeProvider::GetDisplayProperty(int id) const {
return theme_helper_->GetDisplayProperty(id, GetThemeSupplier());
}
bool ThemeService::BrowserThemeProvider::ShouldUseNativeFrame() const {
return theme_helper_->ShouldUseNativeFrame(GetThemeSupplier());
}
bool ThemeService::BrowserThemeProvider::HasCustomImage(int id) const {
return theme_helper_->HasCustomImage(id, GetThemeSupplier());
}
base::RefCountedMemory* ThemeService::BrowserThemeProvider::GetRawData(
int id,
ui::ResourceScaleFactor scale_factor) const {
return theme_helper_->GetRawData(id, GetThemeSupplier(), scale_factor);
}
CustomThemeSupplier* ThemeService::BrowserThemeProvider::GetThemeSupplier()
const {
return incognito_ ? nullptr : delegate_->GetThemeSupplier();
}
// ThemeService ---------------------------------------------------------------
const char ThemeService::kAutogeneratedThemeID[] = "autogenerated_theme_id";
const char ThemeService::kUserColorThemeID[] = "user_color_theme_id";
// static
std::unique_ptr<ui::ThemeProvider> ThemeService::CreateBoundThemeProvider(
Profile* profile,
BrowserThemeProviderDelegate* delegate) {
return std::make_unique<BrowserThemeProvider>(
*ThemeServiceFactory::GetForProfile(profile)->theme_helper_, false,
delegate);
}
ThemeService::ThemeService(Profile* profile, const ThemeHelper& theme_helper)
: profile_(profile),
theme_helper_(theme_helper),
original_theme_provider_(*theme_helper_, false, this),
incognito_theme_provider_(*theme_helper_, true, this) {}
ThemeService::~ThemeService() = default;
void ThemeService::Init() {
theme_helper_->DCheckCalledOnValidSequence();
InitFromPrefs();
// ThemeObserver should be constructed before calling
// OnExtensionServiceReady. Otherwise, the ThemeObserver won't be
// constructed in time to observe the corresponding events.
#if BUILDFLAG(ENABLE_EXTENSIONS)
theme_observer_ = std::make_unique<ThemeObserver>(this);
extensions::ExtensionSystem::Get(profile_)->ready().Post(
FROM_HERE, base::BindOnce(&ThemeService::OnExtensionServiceReady,
weak_ptr_factory_.GetWeakPtr()));
#endif
theme_syncable_service_ =
std::make_unique<ThemeSyncableService>(profile_, this);
// TODO(gayane): Temporary entry point for Chrome Colors. Remove once UI is
// there.
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kInstallAutogeneratedTheme)) {
std::string value =
command_line->GetSwitchValueASCII(switches::kInstallAutogeneratedTheme);
std::vector<std::string> rgb = base::SplitString(
value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (rgb.size() != 3)
return;
int r, g, b;
base::StringToInt(rgb[0], &r);
base::StringToInt(rgb[1], &g);
base::StringToInt(rgb[2], &b);
BuildAutogeneratedThemeFromColor(SkColorSetRGB(r, g, b));
}
pref_change_registrar_.Init(profile_->GetPrefs());
pref_change_registrar_.Add(
prefs::kPolicyThemeColor,
base::BindRepeating(&ThemeService::HandlePolicyColorUpdate,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kBrowserColorScheme,
base::BindRepeating(&ThemeService::NotifyThemeChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kBrowserColorVariant,
base::BindRepeating(&ThemeService::NotifyThemeChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kGrayscaleThemeEnabled,
base::BindRepeating(&ThemeService::NotifyThemeChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
prefs::kUserColor, base::BindRepeating(&ThemeService::NotifyThemeChanged,
base::Unretained(this)));
}
void ThemeService::Shutdown() {
#if BUILDFLAG(ENABLE_EXTENSIONS)
theme_observer_.reset();
#endif
}
CustomThemeSupplier* ThemeService::GetThemeSupplier() const {
return theme_supplier_.get();
}
bool ThemeService::ShouldUseCustomFrame() const {
#if BUILDFLAG(IS_LINUX)
if (!ui::OzonePlatform::GetInstance()
->GetPlatformRuntimeProperties()
.supports_server_side_window_decorations) {
return true;
}
return profile_->GetPrefs()->GetBoolean(prefs::kUseCustomChromeFrame);
#else
return true;
#endif
}
void ThemeService::SetTheme(const extensions::Extension* extension) {
DoSetTheme(extension, true);
}
void ThemeService::RevertToExtensionTheme(const std::string& extension_id) {
const auto* extension = extensions::ExtensionRegistry::Get(profile_)
->disabled_extensions()
.GetByID(extension_id);
if (extension && extension->is_theme()) {
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
DCHECK(!service->IsExtensionEnabled(extension->id()));
// |extension| is disabled when reverting to the previous theme via an
// infobar.
service->EnableExtension(extension->id());
// Enabling the extension will call back to SetTheme().
}
}
void ThemeService::UseTheme(ui::SystemTheme system_theme) {
UseDefaultTheme();
}
void ThemeService::UseDefaultTheme() {
if (UsingPolicyTheme()) {
DVLOG(1)
<< "Default theme was not applied because a policy theme has been set.";
return;
}
if (ready_)
base::RecordAction(base::UserMetricsAction("Themes_Reset"));
ClearThemeData(/*clear_ntp_background=*/true);
NotifyThemeChanged();
}
void ThemeService::UseSystemTheme() {
UseDefaultTheme();
}
void ThemeService::UseDeviceTheme(bool follow) {
#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN)
// This toggle is currently supported on ChromeOS and Windows and we only want
// platforms to set the value if they have a visible toggle.
profile_->GetPrefs()->SetBoolean(prefs::kBrowserFollowsSystemThemeColors,
follow);
NotifyThemeChanged();
#endif
}
bool ThemeService::UsingDeviceTheme() const {
#if BUILDFLAG(IS_CHROMEOS)
const PrefService::Preference* pref = profile_->GetPrefs()->FindPreference(
prefs::kBrowserFollowsSystemThemeColors);
// Ensure we respect previous theme settings for an unset follow theme
// value.
if (pref->IsDefaultValue()) {
return GetIsBaseline() && !UsingExtensionTheme();
}
return pref->GetValue()->GetBool();
#elif BUILDFLAG(IS_WIN)
// Always respect the profile preference on Windows. In the default case the
// preference starts disabled.
return profile_->GetPrefs()
->FindPreference(prefs::kBrowserFollowsSystemThemeColors)
->GetValue()
->GetBool();
#else
// Only ChromeOS and Windows have this toggle.
return false;
#endif
}
bool ThemeService::IsSystemThemeDistinctFromDefaultTheme() const {
return false;
}
bool ThemeService::UsingDefaultTheme() const {
return ThemeHelper::IsDefaultTheme(GetThemeSupplier());
}
bool ThemeService::UsingSystemTheme() const {
return UsingDefaultTheme();
}
bool ThemeService::UsingExtensionTheme() const {
return ThemeHelper::IsExtensionTheme(GetThemeSupplier());
}
bool ThemeService::UsingAutogeneratedTheme() const {
return ThemeHelper::IsAutogeneratedTheme(GetThemeSupplier());
}
std::string ThemeService::GetThemeID() const {
return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
}
bool ThemeService::UsingPolicyTheme() const {
return profile_->GetPrefs()->IsManagedPreference(prefs::kPolicyThemeColor);
}
void ThemeService::RemoveUnusedThemes() {
// We do not want to garbage collect themes on startup (|ready_| is false).
// Themes will get garbage collected after |kRemoveUnusedThemesStartupDelay|.
if (!profile_ || !ready_)
return;
if (number_of_reinstallers_ != 0 || !building_extension_id_.empty()) {
return;
}
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
if (!service)
return;
std::string current_theme = GetThemeID();
std::vector<std::string> remove_list;
const extensions::ExtensionSet extensions =
extensions::ExtensionRegistry::Get(profile_)
->GenerateInstalledExtensionsSet();
extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
for (const auto& extension : extensions) {
if (extension->is_theme() && extension->id() != current_theme) {
// Only uninstall themes which are not disabled or are disabled with
// reason DISABLE_USER_ACTION. We cannot blanket uninstall all disabled
// themes because externally installed themes are initially disabled.
int disable_reason = prefs->GetDisableReasons(extension->id());
if (!prefs->IsExtensionDisabled(extension->id()) ||
disable_reason == extensions::disable_reason::DISABLE_USER_ACTION) {
remove_list.push_back(extension->id());
}
}
}
// TODO: Garbage collect all unused themes. This method misses themes which
// are installed but not loaded because they are blocked by a management
// policy provider.
for (size_t i = 0; i < remove_list.size(); ++i) {
service->UninstallExtension(
remove_list[i], extensions::UNINSTALL_REASON_ORPHANED_THEME, nullptr);
}
}
ThemeSyncableService* ThemeService::GetThemeSyncableService() const {
return theme_syncable_service_.get();
}
// static
const ui::ThemeProvider& ThemeService::GetThemeProviderForProfile(
Profile* profile) {
ThemeService* service = ThemeServiceFactory::GetForProfile(profile);
return profile->IsIncognitoProfile() ? service->incognito_theme_provider_
: service->original_theme_provider_;
}
// static
CustomThemeSupplier* ThemeService::GetThemeSupplierForProfile(
Profile* profile) {
return ThemeServiceFactory::GetForProfile(profile)->GetThemeSupplier();
}
ui::ColorProvider* ThemeService::GetColorProvider() {
// Device theme overrides custom themes.
auto* theme_supplier = UsingDeviceTheme() ? nullptr : GetThemeSupplier();
return ui::ColorProviderManager::Get().GetColorProviderFor(
ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(
theme_supplier));
}
void ThemeService::BuildAutogeneratedThemeFromColor(SkColor color) {
if (UsingPolicyTheme()) {
DVLOG(1) << "Autogenerated theme was not applied because a policy theme"
" has been set.";
return;
}
BuildAutogeneratedThemeFromColor(color, /*store_user_prefs*/ true);
}
void ThemeService::BuildAutogeneratedThemeFromColor(SkColor color,
bool store_in_prefs) {
std::optional<std::string> previous_theme_id;
if (UsingExtensionTheme())
previous_theme_id = GetThemeID();
auto pack = base::MakeRefCounted<BrowserThemePack>(
ui::ColorProviderKey::ThemeInitializerSupplier::ThemeType::
kAutogenerated);
BrowserThemePack::BuildFromColor(color, pack.get());
SwapThemeSupplier(std::move(pack));
if (theme_supplier_) {
if (store_in_prefs) {
SetThemePrefsForColor(color);
// Only disable previous extension theme if new theme is saved to prefs,
// otherwise there may be issues (ex. when unsetting managed theme).
if (previous_theme_id.has_value())
DisableExtension(previous_theme_id.value());
}
NotifyThemeChanged();
}
}
SkColor ThemeService::GetAutogeneratedThemeColor() const {
return profile_->GetPrefs()->GetInteger(prefs::kAutogeneratedThemeColor);
}
void ThemeService::BuildAutogeneratedPolicyTheme() {
BuildAutogeneratedThemeFromColor(GetPolicyThemeColor(),
/*store_user_prefs*/ false);
}
SkColor ThemeService::GetPolicyThemeColor() const {
DCHECK(UsingPolicyTheme());
return profile_->GetPrefs()->GetInteger(prefs::kPolicyThemeColor);
}
void ThemeService::SetBrowserColorScheme(
ThemeService::BrowserColorScheme color_scheme) {
{
base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
profile_->GetPrefs()->SetInteger(prefs::kBrowserColorScheme,
static_cast<int>(color_scheme));
}
NotifyThemeChanged();
}
ThemeService::BrowserColorScheme ThemeService::GetBrowserColorScheme() const {
// If not running ChromeRefresh2023 we should always defer to the system color
// scheme.
return features::IsChromeRefresh2023()
? static_cast<BrowserColorScheme>(
profile_->GetPrefs()->GetInteger(prefs::kBrowserColorScheme))
: BrowserColorScheme::kSystem;
}
void ThemeService::SetUserColor(std::optional<SkColor> user_color) {
{
base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
ClearThemeData(/*clear_ntp_background=*/false);
profile_->GetPrefs()->SetInteger(prefs::kUserColor,
user_color.value_or(SK_ColorTRANSPARENT));
profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, kUserColorThemeID);
}
NotifyThemeChanged();
}
std::optional<SkColor> ThemeService::GetUserColor() const {
auto user_color = profile_->GetPrefs()->GetInteger(prefs::kUserColor);
return user_color == SK_ColorTRANSPARENT ? std::nullopt
: std::optional<SkColor>(user_color);
}
void ThemeService::SetBrowserColorVariant(
ui::mojom::BrowserColorVariant color_variant) {
{
base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
profile_->GetPrefs()->SetInteger(prefs::kBrowserColorVariant,
static_cast<int>(color_variant));
}
NotifyThemeChanged();
}
ui::mojom::BrowserColorVariant ThemeService::GetBrowserColorVariant() const {
return static_cast<ui::mojom::BrowserColorVariant>(
profile_->GetPrefs()->GetInteger(prefs::kBrowserColorVariant));
}
void ThemeService::SetUserColorAndBrowserColorVariant(
SkColor user_color,
ui::mojom::BrowserColorVariant color_variant) {
{
base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
ClearThemeData(/*clear_ntp_background=*/false);
profile_->GetPrefs()->SetInteger(prefs::kUserColor, user_color);
profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, kUserColorThemeID);
profile_->GetPrefs()->SetInteger(prefs::kBrowserColorVariant,
static_cast<int>(color_variant));
}
NotifyThemeChanged();
}
void ThemeService::SetIsGrayscale(bool is_grayscale) {
{
base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
ClearThemeData(/*clear_ntp_background=*/false);
profile_->GetPrefs()->SetBoolean(prefs::kGrayscaleThemeEnabled,
is_grayscale);
}
NotifyThemeChanged();
}
bool ThemeService::GetIsGrayscale() const {
return profile_->GetPrefs()->GetBoolean(prefs::kGrayscaleThemeEnabled);
}
bool ThemeService::GetIsBaseline() const {
// Baseline is defined by the absence of a user color set by the corresponding
// profile pref or the autogenerated theme.
return !GetUserColor().has_value() && !UsingAutogeneratedTheme();
}
// static
void ThemeService::DisableThemePackForTesting() {
g_dont_write_theme_pack_for_testing = true;
}
std::unique_ptr<ThemeService::ThemeReinstaller>
ThemeService::BuildReinstallerForCurrentTheme() {
base::OnceClosure reinstall_callback;
if (UsingExtensionTheme()) {
reinstall_callback =
base::BindOnce(&ThemeService::RevertToExtensionTheme,
weak_ptr_factory_.GetWeakPtr(), GetThemeID());
} else if (UsingAutogeneratedTheme()) {
reinstall_callback = base::BindOnce(
static_cast<void (ThemeService::*)(SkColor)>(
&ThemeService::BuildAutogeneratedThemeFromColor),
weak_ptr_factory_.GetWeakPtr(), GetAutogeneratedThemeColor());
} else {
auto system_theme = ui::SystemTheme::kDefault;
if (auto* theme_supplier = GetThemeSupplier()) {
if (auto* native_theme = theme_supplier->GetNativeTheme())
system_theme = native_theme->system_theme();
}
reinstall_callback = base::BindOnce(
&ThemeService::UseTheme, weak_ptr_factory_.GetWeakPtr(), system_theme);
}
return std::make_unique<ThemeReinstaller>(profile_,
std::move(reinstall_callback));
}
void ThemeService::AddObserver(ThemeServiceObserver* observer) {
observers_.AddObserver(observer);
}
void ThemeService::RemoveObserver(ThemeServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
void ThemeService::SetCustomDefaultTheme(
scoped_refptr<CustomThemeSupplier> theme_supplier) {
if (UsingPolicyTheme()) {
DVLOG(1) << "Custom default theme was not applied because a policy "
"theme has been set.";
return;
}
ClearThemeData(/*clear_ntp_background=*/true);
SwapThemeSupplier(std::move(theme_supplier));
NotifyThemeChanged();
}
ui::SystemTheme ThemeService::GetDefaultSystemTheme() const {
return ui::SystemTheme::kDefault;
}
void ThemeService::ClearThemeData(bool clear_ntp_background) {
if (!ready_)
return;
std::optional<std::string> previous_theme_id;
if (UsingExtensionTheme())
previous_theme_id = GetThemeID();
SwapThemeSupplier(nullptr);
ClearThemePrefs();
if (base::FeatureList::IsEnabled(features::kCustomizeChromeSidePanel) &&
clear_ntp_background) {
NtpCustomBackgroundService::ResetNtpTheme(profile_);
}
// Disable extension after modifying the prefs so that unloading the extension
// doesn't trigger |ClearThemeData| again.
if (previous_theme_id.has_value())
DisableExtension(previous_theme_id.value());
}
void ThemeService::InitFromPrefs() {
FixInconsistentPreferencesIfNeeded();
absl::Cleanup set_ready_cleanup = [this] { this->set_ready(); };
// If theme color policy was set while browser was off, apply it now.
if (UsingPolicyTheme()) {
BuildAutogeneratedPolicyTheme();
return;
}
std::string current_id = GetThemeID();
if (current_id == ThemeHelper::kDefaultThemeID) {
if (GetIsGrayscale()) {
chrome_colors::ChromeColorsService::
RecordDynamicColorOnLoadHistogramForGrayscale();
}
UseTheme(GetDefaultSystemTheme());
return;
}
if (current_id == kAutogeneratedThemeID) {
SkColor color = GetAutogeneratedThemeColor();
BuildAutogeneratedThemeFromColor(color);
chrome_colors::ChromeColorsService::RecordColorOnLoadHistogram(color);
return;
}
if (current_id == kUserColorThemeID) {
const auto user_color = GetUserColor();
if (user_color.has_value()) {
chrome_colors::ChromeColorsService::RecordDynamicColorOnLoadHistogram(
*user_color, GetBrowserColorVariant());
}
return;
}
PrefService* prefs = profile_->GetPrefs();
base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
// If we don't have a file pack, we're updating from an old version.
if (!path.empty()) {
path = path.Append(chrome::kThemePackFilename);
SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id));
if (theme_supplier_) {
base::RecordAction(base::UserMetricsAction("Themes.Loaded"));
return;
}
}
// Else: wait for the extension service to be ready so that the theme pack
// can be recreated from the extension.
std::move(set_ready_cleanup).Cancel();
}
void ThemeService::NotifyThemeChanged() {
if (!ready_ || should_suppress_theme_updates_) {
return;
}
// Redraw and notify sync that theme has changed.
for (auto& observer : observers_)
observer.OnThemeChanged();
}
void ThemeService::FixInconsistentPreferencesIfNeeded() {}
void ThemeService::DoSetTheme(const extensions::Extension* extension,
bool suppress_infobar) {
DCHECK(extension->is_theme());
DCHECK(!UsingPolicyTheme());
DCHECK(extensions::ExtensionSystem::Get(profile_)
->extension_service()
->IsExtensionEnabled(extension->id()));
BuildFromExtension(extension, suppress_infobar);
}
void ThemeService::OnExtensionServiceReady() {
if (!ready_) {
// If the ThemeService is not ready yet, the custom theme data pack needs to
// be recreated from the extension.
MigrateTheme();
set_ready();
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ThemeService::RemoveUnusedThemes,
weak_ptr_factory_.GetWeakPtr()),
kRemoveUnusedThemesStartupDelay);
}
void ThemeService::MigrateTheme() {
TRACE_EVENT0("browser", "ThemeService::MigrateTheme");
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile_);
const extensions::Extension* extension =
registry ? registry->enabled_extensions().GetByID(GetThemeID()) : nullptr;
if (extension) {
// Theme migration is done on the UI thread. Blocking the UI from appearing
// until it's ready is deemed better than showing a blip of the default
// theme.
TRACE_EVENT0("browser", "ThemeService::MigrateTheme - BuildFromExtension");
DLOG(ERROR) << "Migrating theme";
scoped_refptr<BrowserThemePack> pack(new BrowserThemePack(
ui::ColorProviderKey::ThemeInitializerSupplier::ThemeType::kExtension));
BrowserThemePack::BuildFromExtension(extension, pack.get());
OnThemeBuiltFromExtension(extension->id(), pack.get(), true);
base::RecordAction(base::UserMetricsAction("Themes.Migrated"));
} else {
DLOG(ERROR) << "Theme is mysteriously gone.";
ClearThemeData(/*clear_ntp_background=*/true);
base::RecordAction(base::UserMetricsAction("Themes.Gone"));
}
}
void ThemeService::SwapThemeSupplier(
scoped_refptr<CustomThemeSupplier> theme_supplier) {
if (theme_supplier_)
theme_supplier_->StopUsingTheme();
theme_supplier_ = theme_supplier;
if (theme_supplier_)
theme_supplier_->StartUsingTheme();
}
void ThemeService::BuildFromExtension(const extensions::Extension* extension,
bool suppress_infobar) {
build_extension_task_tracker_.TryCancelAll();
building_extension_id_ = extension->id();
scoped_refptr<BrowserThemePack> pack(new BrowserThemePack(
ui::ColorProviderKey::ThemeInitializerSupplier::ThemeType::kExtension));
auto task_runner = base::ThreadPool::CreateTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING});
build_extension_task_tracker_.PostTaskAndReply(
task_runner.get(), FROM_HERE,
base::BindOnce(&BrowserThemePack::BuildFromExtension,
base::RetainedRef(extension),
base::RetainedRef(pack.get())),
base::BindOnce(&ThemeService::OnThemeBuiltFromExtension,
weak_ptr_factory_.GetWeakPtr(), extension->id(), pack,
suppress_infobar));
}
void ThemeService::OnThemeBuiltFromExtension(
const extensions::ExtensionId& extension_id,
scoped_refptr<BrowserThemePack> pack,
bool suppress_infobar) {
if (UsingPolicyTheme()) {
DVLOG(1) << "Extension theme was not applied because a policy theme has "
"been set.";
return;
}
if (!pack->is_valid()) {
// TODO(erg): We've failed to install the theme; perhaps we should tell the
// user? http://crbug.com/34780
LOG(ERROR) << "Could not load theme.";
return;
}
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
if (!service)
return;
const auto* extension = extensions::ExtensionRegistry::Get(profile_)
->enabled_extensions()
.GetByID(extension_id);
if (!extension)
return;
// Schedule the writing of the packed file to disk.
extensions::GetExtensionFileTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&WritePackToDiskCallback,
base::RetainedRef(pack), extension->path()));
std::unique_ptr<ThemeService::ThemeReinstaller> reinstaller =
BuildReinstallerForCurrentTheme();
std::optional<std::string> previous_theme_id;
if (UsingExtensionTheme())
previous_theme_id = GetThemeID();
SwapThemeSupplier(std::move(pack));
SetThemePrefsForExtension(extension);
NotifyThemeChanged();
building_extension_id_.clear();
// Same old theme, but the theme has changed (migrated) or auto-updated.
if (previous_theme_id.has_value() &&
previous_theme_id.value() == extension->id()) {
return;
}
base::RecordAction(base::UserMetricsAction("Themes_Installed"));
bool can_revert_theme = true;
if (previous_theme_id.has_value())
can_revert_theme = DisableExtension(previous_theme_id.value());
// Offer to revert to the old theme.
if (can_revert_theme && !suppress_infobar && extension->is_theme()) {
// FindTabbedBrowser() is called with |match_original_profiles| true because
// a theme install in either a normal or incognito window for a profile
// affects all normal and incognito windows for that profile.
Browser* browser = chrome::FindTabbedBrowser(profile_, true);
if (browser) {
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (web_contents) {
ThemeInstalledInfoBarDelegate::Create(
infobars::ContentInfoBarManager::FromWebContents(web_contents),
ThemeServiceFactory::GetForProfile(profile_), extension->name(),
extension->id(), std::move(reinstaller));
}
}
}
}
void ThemeService::HandlePolicyColorUpdate() {
if (UsingPolicyTheme()) {
BuildAutogeneratedPolicyTheme();
} else {
// If a policy theme is unset, load the previous theme from prefs.
InitFromPrefs();
// NotifyThemeChanged() isn't triggered in InitFromPrefs() for extension
// themes, so it's called here to make sure the browser's theme is updated.
if (UsingExtensionTheme())
NotifyThemeChanged();
}
}
void ThemeService::ClearThemePrefs() {
profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
profile_->GetPrefs()->ClearPref(prefs::kAutogeneratedThemeColor);
profile_->GetPrefs()->ClearPref(prefs::kUserColor);
profile_->GetPrefs()->ClearPref(prefs::kGrayscaleThemeEnabled);
profile_->GetPrefs()->ClearPref(prefs::kBrowserColorVariant);
profile_->GetPrefs()->SetString(prefs::kCurrentThemeID,
ThemeHelper::kDefaultThemeID);
}
void ThemeService::SetThemePrefsForExtension(
const extensions::Extension* extension) {
ClearThemePrefs();
if (base::FeatureList::IsEnabled(features::kCustomizeChromeSidePanel)) {
NtpCustomBackgroundService::ResetNtpTheme(profile_);
// Extensions are incompatible with device themes so turn them off.
// TODO(crbug.com/40280173): Remove this if we can otherwise separate
// extension and device themes from attempting to apply at the same time.
profile_->GetPrefs()->SetBoolean(prefs::kBrowserFollowsSystemThemeColors,
false);
}
profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, extension->id());
// Save only the extension path. The packed file will be loaded via
// InitFromPrefs().
profile_->GetPrefs()->SetFilePath(prefs::kCurrentThemePackFilename,
extension->path());
}
void ThemeService::SetThemePrefsForColor(SkColor color) {
ClearThemePrefs();
profile_->GetPrefs()->SetInteger(prefs::kAutogeneratedThemeColor, color);
profile_->GetPrefs()->SetString(prefs::kCurrentThemeID,
kAutogeneratedThemeID);
}
bool ThemeService::DisableExtension(const std::string& extension_id) {
extensions::ExtensionService* service =
extensions::ExtensionSystem::Get(profile_)->extension_service();
if (!service)
return false;
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile_);
if (registry->GetInstalledExtension(extension_id)) {
// Do not disable the previous theme if it is already uninstalled. Sending
// |ThemeServiceObserver::OnThemeChanged()| causes the previous theme to be
// uninstalled when the notification causes the remaining infobar to close
// and does not open any new infobars. See crbug.com/468280.
service->DisableExtension(extension_id,
extensions::disable_reason::DISABLE_USER_ACTION);
return true;
}
return false;
}