From 14871f134649aaaff6818c08fd744f08ed065307 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 May 2022 23:06:29 +0200 Subject: [PATCH] [Linux] read settings from XDG desktop portal if available (#33100) * [Linux] read settings from XDG desktop portal if available Fixes: flutter/flutter#101438 --- ci/licenses_golden/licenses_flutter | 3 + shell/platform/linux/BUILD.gn | 2 + shell/platform/linux/fl_settings.cc | 12 +- shell/platform/linux/fl_settings_portal.cc | 262 ++++++++++++++++++ shell/platform/linux/fl_settings_portal.h | 56 ++++ .../platform/linux/fl_settings_portal_test.cc | 111 ++++++++ 6 files changed, 444 insertions(+), 2 deletions(-) create mode 100644 shell/platform/linux/fl_settings_portal.cc create mode 100644 shell/platform/linux/fl_settings_portal.h create mode 100644 shell/platform/linux/fl_settings_portal_test.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 7463d8232ef35..4232df30490d0 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2182,6 +2182,9 @@ FILE: ../../../flutter/shell/platform/linux/fl_settings.h FILE: ../../../flutter/shell/platform/linux/fl_settings_plugin.cc FILE: ../../../flutter/shell/platform/linux/fl_settings_plugin.h FILE: ../../../flutter/shell/platform/linux/fl_settings_plugin_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.cc +FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.h +FILE: ../../../flutter/shell/platform/linux/fl_settings_portal_test.cc FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec_private.h FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec_test.cc diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 6c78359e0734e..38dab1de0b4da 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -131,6 +131,7 @@ source_set("flutter_linux_sources") { "fl_renderer_headless.cc", "fl_settings.cc", "fl_settings_plugin.cc", + "fl_settings_portal.cc", "fl_standard_message_codec.cc", "fl_standard_method_codec.cc", "fl_string_codec.cc", @@ -208,6 +209,7 @@ executable("flutter_linux_unittests") { "fl_pixel_buffer_texture_test.cc", "fl_plugin_registrar_test.cc", "fl_settings_plugin_test.cc", + "fl_settings_portal_test.cc", "fl_standard_message_codec_test.cc", "fl_standard_method_codec_test.cc", "fl_string_codec_test.cc", diff --git a/shell/platform/linux/fl_settings.cc b/shell/platform/linux/fl_settings.cc index c129da3146d26..e464c46354951 100644 --- a/shell/platform/linux/fl_settings.cc +++ b/shell/platform/linux/fl_settings.cc @@ -4,6 +4,7 @@ #include "flutter/shell/platform/linux/fl_settings.h" #include "flutter/shell/platform/linux/fl_gnome_settings.h" +#include "flutter/shell/platform/linux/fl_settings_portal.h" G_DEFINE_INTERFACE(FlSettings, fl_settings, G_TYPE_OBJECT) @@ -44,6 +45,13 @@ void fl_settings_emit_changed(FlSettings* self) { } FlSettings* fl_settings_new() { - // TODO(jpnurmi): add support for other desktop environments - return FL_SETTINGS(fl_gnome_settings_new()); + g_autoptr(FlSettingsPortal) portal = fl_settings_portal_new(); + + g_autoptr(GError) error = nullptr; + if (!fl_settings_portal_start(portal, &error)) { + g_debug("XDG desktop portal settings unavailable: %s", error->message); + return fl_gnome_settings_new(); + } + + return FL_SETTINGS(g_object_ref(portal)); } diff --git a/shell/platform/linux/fl_settings_portal.cc b/shell/platform/linux/fl_settings_portal.cc new file mode 100644 index 0000000000000..455904f4ad019 --- /dev/null +++ b/shell/platform/linux/fl_settings_portal.cc @@ -0,0 +1,262 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/linux/fl_settings_portal.h" + +#include +#include + +static constexpr char kPortalName[] = "org.freedesktop.portal.Desktop"; +static constexpr char kPortalPath[] = "/org/freedesktop/portal/desktop"; +static constexpr char pPortalSettings[] = "org.freedesktop.portal.Settings"; + +struct FlSetting { + const gchar* ns; + const gchar* key; + const GVariantType* type; +}; + +static constexpr char kXdgAppearance[] = "org.freedesktop.appearance"; +static const FlSetting kColorScheme = { + kXdgAppearance, + "color-scheme", + G_VARIANT_TYPE_UINT32, +}; + +static constexpr char kGnomeDesktopInterface[] = "org.gnome.desktop.interface"; +static const FlSetting kClockFormat = { + kGnomeDesktopInterface, + "clock-format", + G_VARIANT_TYPE_STRING, +}; +static const FlSetting kGtkTheme = { + kGnomeDesktopInterface, + "gtk-theme", + G_VARIANT_TYPE_STRING, +}; +static const FlSetting kTextScalingFactor = { + kGnomeDesktopInterface, + "text-scaling-factor", + G_VARIANT_TYPE_DOUBLE, +}; + +static const FlSetting all_settings[] = { + kClockFormat, + kColorScheme, + kGtkTheme, + kTextScalingFactor, +}; + +static constexpr char kClockFormat12Hour[] = "12h"; +static constexpr char kGtkThemeDarkSuffix[] = "-dark"; + +typedef enum { DEFAULT, PREFER_DARK, PREFER_LIGHT } ColorScheme; + +struct _FlSettingsPortal { + GObject parent_instance; + + GDBusProxy* dbus_proxy; + GVariantDict* values; +}; + +static void fl_settings_portal_iface_init(FlSettingsInterface* iface); + +G_DEFINE_TYPE_WITH_CODE(FlSettingsPortal, + fl_settings_portal, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(fl_settings_get_type(), + fl_settings_portal_iface_init)) + +static gchar* format_key(const FlSetting* setting) { + return g_strconcat(setting->ns, "::", setting->key, nullptr); +} + +static gboolean get_value(FlSettingsPortal* portal, + const FlSetting* setting, + GVariant** value) { + g_autofree const gchar* key = format_key(setting); + *value = g_variant_dict_lookup_value(portal->values, key, setting->type); + return *value != nullptr; +} + +static void set_value(FlSettingsPortal* portal, + const FlSetting* setting, + GVariant* value) { + g_autofree const gchar* key = format_key(setting); + + // ignore redundant changes from multiple XDG desktop portal backends + g_autoptr(GVariant) old_value = + g_variant_dict_lookup_value(portal->values, key, nullptr); + if (old_value != nullptr && value != nullptr && + g_variant_equal(old_value, value)) { + return; + } + + g_variant_dict_insert_value(portal->values, key, value); + fl_settings_emit_changed(FL_SETTINGS(portal)); +} + +// Based on +// https://gitlab.gnome.org/GNOME/Initiatives/-/wikis/Dark-Style-Preference#other +static gboolean settings_portal_read(GDBusProxy* proxy, + const gchar* ns, + const gchar* key, + GVariant** out) { + g_autoptr(GError) error = nullptr; + g_autoptr(GVariant) value = + g_dbus_proxy_call_sync(proxy, "Read", g_variant_new("(ss)", ns, key), + G_DBUS_CALL_FLAGS_NONE, G_MAXINT, nullptr, &error); + + if (error) { + if (error->domain == G_DBUS_ERROR && + error->code == G_DBUS_ERROR_SERVICE_UNKNOWN) { + g_debug("XDG desktop portal unavailable: %s", error->message); + return false; + } + + if (error->domain == G_DBUS_ERROR && + error->code == G_DBUS_ERROR_UNKNOWN_METHOD) { + g_debug("XDG desktop portal settings unavailable: %s", error->message); + return false; + } + + g_critical("Failed to read XDG desktop portal settings: %s", + error->message); + return false; + } + + g_autoptr(GVariant) child = nullptr; + g_variant_get(value, "(v)", &child); + g_variant_get(child, "v", out); + + return true; +} + +static void settings_portal_changed_cb(GDBusProxy* proxy, + const char* sender_name, + const char* signal_name, + GVariant* parameters, + gpointer user_data) { + FlSettingsPortal* portal = FL_SETTINGS_PORTAL(user_data); + if (g_strcmp0(signal_name, "SettingChanged")) { + return; + } + + FlSetting setting; + g_autoptr(GVariant) value = nullptr; + g_variant_get(parameters, "(&s&sv)", &setting.ns, &setting.key, &value); + set_value(portal, &setting, value); +} + +static FlClockFormat fl_settings_portal_get_clock_format(FlSettings* settings) { + FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings); + + FlClockFormat clock_format = FL_CLOCK_FORMAT_24H; + + g_autoptr(GVariant) value = nullptr; + if (get_value(self, &kClockFormat, &value)) { + const gchar* clock_format_str = g_variant_get_string(value, nullptr); + if (g_strcmp0(clock_format_str, kClockFormat12Hour) == 0) { + clock_format = FL_CLOCK_FORMAT_12H; + } + } + + return clock_format; +} + +static FlColorScheme fl_settings_portal_get_color_scheme(FlSettings* settings) { + FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings); + + FlColorScheme color_scheme = FL_COLOR_SCHEME_LIGHT; + + g_autoptr(GVariant) value = nullptr; + if (get_value(self, &kColorScheme, &value)) { + if (g_variant_get_uint32(value) == PREFER_DARK) { + color_scheme = FL_COLOR_SCHEME_DARK; + } + } else if (get_value(self, &kGtkTheme, &value)) { + const gchar* gtk_theme_str = g_variant_get_string(value, nullptr); + if (g_str_has_suffix(gtk_theme_str, kGtkThemeDarkSuffix)) { + color_scheme = FL_COLOR_SCHEME_DARK; + } + } + + return color_scheme; +} + +static gdouble fl_settings_portal_get_text_scaling_factor( + FlSettings* settings) { + FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings); + + gdouble scaling_factor = 1.0; + + g_autoptr(GVariant) value = nullptr; + if (get_value(self, &kTextScalingFactor, &value)) { + scaling_factor = g_variant_get_double(value); + } + + return scaling_factor; +} + +static void fl_settings_portal_dispose(GObject* object) { + FlSettingsPortal* self = FL_SETTINGS_PORTAL(object); + + g_clear_object(&self->dbus_proxy); + g_clear_pointer(&self->values, g_variant_dict_unref); + + G_OBJECT_CLASS(fl_settings_portal_parent_class)->dispose(object); +} + +static void fl_settings_portal_class_init(FlSettingsPortalClass* klass) { + GObjectClass* object_class = G_OBJECT_CLASS(klass); + object_class->dispose = fl_settings_portal_dispose; +} + +static void fl_settings_portal_iface_init(FlSettingsInterface* iface) { + iface->get_clock_format = fl_settings_portal_get_clock_format; + iface->get_color_scheme = fl_settings_portal_get_color_scheme; + iface->get_text_scaling_factor = fl_settings_portal_get_text_scaling_factor; +} + +static void fl_settings_portal_init(FlSettingsPortal* self) {} + +FlSettingsPortal* fl_settings_portal_new() { + g_autoptr(GVariantDict) values = g_variant_dict_new(nullptr); + return fl_settings_portal_new_with_values(values); +} + +FlSettingsPortal* fl_settings_portal_new_with_values(GVariantDict* values) { + g_return_val_if_fail(values != nullptr, nullptr); + FlSettingsPortal* portal = + FL_SETTINGS_PORTAL(g_object_new(fl_settings_portal_get_type(), nullptr)); + portal->values = g_variant_dict_ref(values); + return portal; +} + +gboolean fl_settings_portal_start(FlSettingsPortal* self, GError** error) { + g_return_val_if_fail(FL_IS_SETTINGS_PORTAL(self), false); + g_return_val_if_fail(self->dbus_proxy == nullptr, false); + + self->dbus_proxy = g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, kPortalName, + kPortalPath, pPortalSettings, nullptr, error); + + if (self->dbus_proxy == nullptr) { + return false; + } + + for (const FlSetting setting : all_settings) { + g_autoptr(GVariant) value = nullptr; + if (settings_portal_read(self->dbus_proxy, setting.ns, setting.key, + &value)) { + set_value(self, &setting, value); + } + } + + g_signal_connect_object(self->dbus_proxy, "g-signal", + G_CALLBACK(settings_portal_changed_cb), self, + GConnectFlags(0)); + + return true; +} diff --git a/shell/platform/linux/fl_settings_portal.h b/shell/platform/linux/fl_settings_portal.h new file mode 100644 index 0000000000000..cf8d2544d1d4b --- /dev/null +++ b/shell/platform/linux/fl_settings_portal.h @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_SETTINGS_PORTAL_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_SETTINGS_PORTAL_H_ + +#include "flutter/shell/platform/linux/fl_settings.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlSettingsPortal, + fl_settings_portal, + FL, + SETTINGS_PORTAL, + GObject); + +/** + * FlSettingsPortal: + * #FlSettingsPortal reads settings from the XDG desktop portal. + */ + +/** + * fl_settings_portal_new: + * + * Creates a new settings portal instance. + * + * Returns: a new #FlSettingsPortal. + */ +FlSettingsPortal* fl_settings_portal_new(); + +/** + * fl_settings_portal_new_with_values: + * @values: (nullable): a #GVariantDict. + * + * Creates a new settings portal instance with initial values for testing. + * + * Returns: a new #FlSettingsPortal. + */ +FlSettingsPortal* fl_settings_portal_new_with_values(GVariantDict* values); + +/** + * fl_settings_portal_start: + * @portal: an #FlSettingsPortal. + * @error: (allow-none): #GError location to store the error occurring, or %NULL + * + * Reads the current settings and starts monitoring for changes in the desktop + * portal settings. + * + * Returns: %TRUE on success, or %FALSE if the portal is not available. + */ +gboolean fl_settings_portal_start(FlSettingsPortal* portal, GError** error); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_SETTINGS_PORTAL_H_ diff --git a/shell/platform/linux/fl_settings_portal_test.cc b/shell/platform/linux/fl_settings_portal_test.cc new file mode 100644 index 0000000000000..4130a8cb1a84d --- /dev/null +++ b/shell/platform/linux/fl_settings_portal_test.cc @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/linux/fl_settings_portal.h" +#include "flutter/shell/platform/linux/testing/fl_test.h" +#include "flutter/testing/testing.h" + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +TEST(FlSettingsPortalTest, ClockFormat) { + g_autoptr(GVariantDict) settings = g_variant_dict_new(nullptr); + + g_autoptr(FlSettings) portal = + FL_SETTINGS(fl_settings_portal_new_with_values(settings)); + EXPECT_EQ(fl_settings_get_clock_format(portal), FL_CLOCK_FORMAT_24H); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::clock-format", + g_variant_new_string("24h")); + EXPECT_EQ(fl_settings_get_clock_format(portal), FL_CLOCK_FORMAT_24H); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::clock-format", + g_variant_new_string("12h")); + EXPECT_EQ(fl_settings_get_clock_format(portal), FL_CLOCK_FORMAT_12H); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::clock-format", + g_variant_new_string("unknown")); + EXPECT_EQ(fl_settings_get_clock_format(portal), FL_CLOCK_FORMAT_24H); +} + +TEST(FlSettingsPortalTest, ColorScheme) { + g_autoptr(GVariantDict) settings = g_variant_dict_new(nullptr); + + g_autoptr(FlSettings) portal = + FL_SETTINGS(fl_settings_portal_new_with_values(settings)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + g_variant_dict_insert_value(settings, + "org.freedesktop.appearance::color-scheme", + g_variant_new_uint32(1)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_DARK); + + g_variant_dict_insert_value(settings, + "org.freedesktop.appearance::color-scheme", + g_variant_new_uint32(2)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + g_variant_dict_insert_value(settings, + "org.freedesktop.appearance::color-scheme", + g_variant_new_uint32(123)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + // color-scheme takes precedence over gtk-theme + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::gtk-theme", + g_variant_new_string("Yaru-dark")); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); +} + +TEST(FlSettingsPortalTest, GtkTheme) { + g_autoptr(GVariantDict) settings = g_variant_dict_new(nullptr); + + g_autoptr(FlSettings) portal = + FL_SETTINGS(fl_settings_portal_new_with_values(settings)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::gtk-theme", + g_variant_new_string("Yaru-dark")); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_DARK); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::gtk-theme", + g_variant_new_string("Yaru")); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::gtk-theme", + g_variant_new_string("Adwaita")); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); + + g_variant_dict_insert_value(settings, + "org.gnome.desktop.interface::gtk-theme", + g_variant_new_string("Adwaita-dark")); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_DARK); + + // color-scheme takes precedence over gtk-theme + g_variant_dict_insert_value(settings, + "org.freedesktop.appearance::color-scheme", + g_variant_new_uint32(2)); + EXPECT_EQ(fl_settings_get_color_scheme(portal), FL_COLOR_SCHEME_LIGHT); +} + +TEST(FlSettingsPortalTest, TextScalingFactor) { + g_autoptr(GVariantDict) settings = g_variant_dict_new(nullptr); + + g_autoptr(FlSettings) portal = + FL_SETTINGS(fl_settings_portal_new_with_values(settings)); + EXPECT_EQ(fl_settings_get_text_scaling_factor(portal), 1.0); + + g_variant_dict_insert_value( + settings, "org.gnome.desktop.interface::text-scaling-factor", + g_variant_new_double(1.5)); + EXPECT_EQ(fl_settings_get_text_scaling_factor(portal), 1.5); +}