| // 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; |
| } |