diff --git a/bin/system-info.c b/bin/system-info.c
index 18c922f6be26cccf4c12b38a70df27dc8ef843e4..02f4ce0d1d0d1979746e4b16999885a52fac68e2 100644
--- a/bin/system-info.c
+++ b/bin/system-info.c
@@ -323,6 +323,13 @@ jsonify_locale_issues (JsonBuilder *builder,
   jsonify_flags (builder, SRT_TYPE_LOCALE_ISSUES, issues);
 }
 
+static void
+jsonify_xdg_portal_issues (JsonBuilder *builder,
+                           SrtXdgPortalIssues issues)
+{
+  jsonify_flags (builder, SRT_TYPE_XDG_PORTAL_ISSUES, issues);
+}
+
 static void
 jsonify_x86_features (JsonBuilder *builder,
                       SrtX86FeatureFlags present,
@@ -781,8 +788,11 @@ main (int argc,
   SrtSteamIssues steam_issues = SRT_STEAM_ISSUES_NONE;
   SrtRuntimeIssues runtime_issues = SRT_RUNTIME_ISSUES_NONE;
   SrtLocaleIssues locale_issues = SRT_LOCALE_ISSUES_NONE;
+  SrtXdgPortalIssues xdg_portal_issues = SRT_XDG_PORTAL_ISSUES_NONE;
   SrtX86FeatureFlags x86_features = SRT_X86_FEATURE_NONE;
   SrtX86FeatureFlags known_x86_features = SRT_X86_FEATURE_NONE;
+  g_autoptr(SrtObjectList) portal_backends = NULL;
+  g_autoptr(SrtObjectList) portal_interfaces = NULL;
   char *expectations = NULL;
   gboolean verbose = FALSE;
   JsonBuilder *builder;
@@ -791,6 +801,7 @@ main (int argc,
   const gchar *test_json_path = NULL;
   g_autofree gchar *steamscript_path = NULL;
   g_autofree gchar *steamscript_version = NULL;
+  g_autofree gchar *xdg_portal_messages = NULL;
   gchar *json_output;
   gchar *version = NULL;
   gchar *inst_path = NULL;
@@ -1285,6 +1296,69 @@ main (int argc,
     }
   json_builder_end_array (builder);
 
+  json_builder_set_member_name (builder, "xdg-portals");
+  json_builder_begin_object (builder);
+    {
+      portal_interfaces = srt_system_info_list_xdg_portal_interfaces (info);
+      portal_backends = srt_system_info_list_xdg_portal_backends (info);
+      if (portal_interfaces != NULL || portal_backends != NULL)
+        {
+          json_builder_set_member_name (builder, "details");
+          json_builder_begin_object (builder);
+
+          if (portal_interfaces != NULL)
+            {
+              json_builder_set_member_name (builder, "interfaces");
+              json_builder_begin_object (builder);
+              for (const GList *iter = portal_interfaces; iter != NULL; iter = iter->next)
+                {
+                  gboolean is_available;
+                  json_builder_set_member_name (builder, srt_xdg_portal_interface_get_name (iter->data));
+                  json_builder_begin_object (builder);
+                  json_builder_set_member_name (builder, "available");
+                  is_available = srt_xdg_portal_interface_is_available (iter->data);
+                  json_builder_add_boolean_value (builder, is_available);
+                  if (is_available)
+                    {
+                      json_builder_set_member_name (builder, "version");
+                      json_builder_add_int_value (builder, srt_xdg_portal_interface_get_version (iter->data));
+                    }
+                  json_builder_end_object (builder);
+                }
+              json_builder_end_object (builder);
+            }
+
+          if (portal_backends != NULL)
+            {
+              json_builder_set_member_name (builder, "backends");
+              json_builder_begin_object (builder);
+              for (const GList *iter = portal_backends; iter != NULL; iter = iter->next)
+                {
+                  json_builder_set_member_name (builder, srt_xdg_portal_backend_get_name (iter->data));
+                  json_builder_begin_object (builder);
+                  json_builder_set_member_name (builder, "available");
+                  json_builder_add_boolean_value (builder, srt_xdg_portal_backend_is_available (iter->data));
+                  json_builder_end_object (builder);
+                }
+              json_builder_end_object (builder);
+            }
+          json_builder_end_object (builder);
+        }
+
+      json_builder_set_member_name (builder, "issues");
+      json_builder_begin_array (builder);
+      xdg_portal_issues = srt_system_info_get_xdg_portal_issues (info, &xdg_portal_messages);
+      jsonify_xdg_portal_issues (builder, xdg_portal_issues);
+      json_builder_end_array (builder);
+
+      if (xdg_portal_messages != NULL)
+        {
+          json_builder_set_member_name (builder, "messages");
+          json_builder_add_string_value (builder, xdg_portal_messages);
+        }
+    }
+  json_builder_end_object (builder);
+
   json_builder_set_member_name (builder, "cpu-features");
   json_builder_begin_object (builder);
     {
diff --git a/bin/system-info.md b/bin/system-info.md
index 25b896cc0d97d3a070ce20801344553c0e835886..95aa8d9c2d398ad5f2278c898d21b462ae7c2aed 100644
--- a/bin/system-info.md
+++ b/bin/system-info.md
@@ -681,6 +681,55 @@ keys:
     **steam_uri_handler**
     :   A boolean value indicating whether this entry can open `steam:` URIs.
 
+**xdg-portals**
+:   An object describing the XDG portal support. Whether `xdg-desktop-portal`
+    is installed, and working, and which portal backend implementations are
+    available. The keys are the strings:
+
+    **details**
+    :   An object describing in details which XDG portals have been checked.
+        The keys are the strings:
+
+        **interfaces**
+        :   An object describing XDG portal interfaces. The keys are the
+            interface name. The values are objects containing details of
+            of an interface:
+
+            **available**
+            :   A boolean value indicating whether this interface is available.
+
+            **version**
+            :   An integer representing the version property of this interface.
+
+        **backends**
+        :   An object describing XDG portal backends. The keys are the
+            backend name. The values are objects containing details of
+            of an interface:
+
+            **available**
+            :   A boolean value indicating whether this backend is available.
+
+    **issues**
+    :   Problems with the XDG portal support, represented as an array
+        of strings. The array is empty if no problems were detected.
+
+        **unknown**
+        :   There was an unknown error while checking XDG portal support.
+
+        **timeout**
+        :   The test to check the XDG portal support timed out.
+
+        **missing-interface**
+        :   At least one of the required XDG portal interfaces is missing.
+
+        **no-implementation**
+        :   There isn't a working XDG portal implementation.
+
+    **messages**
+    :   Human-readable string with error messages about the XDG portal
+        support. This element will not be printed if there aren't any error
+        messages.
+
 **cpu-features**
 :   An object decribing some of the features that the CPU in use supports.
     Currently it has the following string keys, each with a boolean
diff --git a/helpers/check-xdg-portal.c b/helpers/check-xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..f0869c7f22a446574b8aad50b6d9611f7b9934c6
--- /dev/null
+++ b/helpers/check-xdg-portal.c
@@ -0,0 +1,255 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+#include <steam-runtime-tools/glib-backports-internal.h>
+#include <steam-runtime-tools/json-glib-compat.h>
+#include <steam-runtime-tools/utils-internal.h>
+
+#if !GLIB_CHECK_VERSION(2, 43, 4)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDBusProxy, g_object_unref)
+#endif
+
+static gboolean opt_print_version = FALSE;
+
+static const GOptionEntry option_entries[] =
+{
+  { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version,
+    "Print version number and exit", NULL },
+  { NULL }
+};
+
+int
+main (int argc,
+      char **argv)
+{
+  g_autofree gchar *json = NULL;
+  g_autoptr(FILE) original_stdout = NULL;
+  g_autoptr(JsonNode) root = NULL;
+  g_autoptr(JsonBuilder) builder = NULL;
+  g_autoptr(JsonGenerator) generator = NULL;
+  g_autoptr(GDBusConnection) connection = NULL;
+  g_autoptr(GVariant) variant_version = NULL;
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(GOptionContext) option_context = NULL;
+  guint32 version;
+  GError **error = &local_error;
+  gboolean impl_available = FALSE;
+  int ret = EXIT_SUCCESS;
+  gsize i;
+
+  static const gchar * const portal_interface_name[] = {
+    "org.freedesktop.portal.OpenURI",
+    "org.freedesktop.portal.Email",
+    NULL,
+  };
+
+  static const gchar * const portal_impl_name[] = {
+    "org.freedesktop.impl.portal.desktop.gtk",
+    "org.freedesktop.impl.portal.desktop.kde",
+    NULL,
+  };
+
+  option_context = g_option_context_new ("");
+  g_option_context_add_main_entries (option_context, option_entries, NULL);
+
+  if (!g_option_context_parse (option_context, &argc, &argv, &local_error))
+    return 2;
+
+  if (opt_print_version)
+    {
+      /* Output version number as YAML for machine-readability,
+       * inspired by `ostree --version` and `docker version` */
+      g_print (
+          "%s:\n"
+          " Package: steam-runtime-tools\n"
+          " Version: %s\n",
+          argv[0], VERSION);
+      return EXIT_SUCCESS;
+    }
+
+  /* stdout is reserved for machine-readable output, so avoid having
+   * things like g_debug() pollute it. */
+  original_stdout = _srt_divert_stdout_to_stderr (error);
+
+  if (original_stdout == NULL)
+    {
+      g_printerr ("Unable to divert stdout to stderr: %s", local_error->message);
+      return EXIT_FAILURE;
+    }
+
+  builder = json_builder_new ();
+  json_builder_begin_object (builder);
+
+  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error);
+  if (connection == NULL)
+    {
+      g_printerr ("Unable to get the session bus: %s\n", local_error->message);
+      return EXIT_FAILURE;
+    }
+
+  json_builder_set_member_name (builder, "interfaces");
+  json_builder_begin_object (builder);
+  for (i = 0; portal_interface_name[i] != NULL; i++)
+    {
+      g_autoptr(GDBusProxy) portal_proxy = NULL;
+      g_autofree gchar *name_owner = NULL;
+      portal_proxy = g_dbus_proxy_new_sync (connection,
+                                            G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+                                            NULL,
+                                            "org.freedesktop.portal.Desktop",
+                                            "/org/freedesktop/portal/desktop",
+                                            portal_interface_name[i],
+                                            NULL,
+                                            error);
+
+      json_builder_set_member_name (builder, portal_interface_name[i]);
+      json_builder_begin_object (builder);
+      json_builder_set_member_name (builder, "available");
+
+      if (portal_proxy == NULL)
+        {
+          g_printerr ("Failed to contact 'org.freedesktop.portal.Desktop': %s\n",
+                      local_error->message);
+          json_builder_add_boolean_value (builder, FALSE);
+          json_builder_end_object (builder);
+          ret = EXIT_FAILURE;
+          continue;
+        }
+
+      name_owner = g_dbus_proxy_get_name_owner (portal_proxy);
+      if (name_owner == NULL)
+        {
+          g_printerr ("Failed to connect to 'org.freedesktop.portal.Desktop'\n");
+          json_builder_add_boolean_value (builder, FALSE);
+          json_builder_end_object (builder);
+          ret = EXIT_FAILURE;
+          continue;
+        }
+
+      variant_version = g_dbus_proxy_get_cached_property (portal_proxy, "version");
+
+      if (variant_version == NULL
+          || g_variant_classify (variant_version) != G_VARIANT_CLASS_UINT32)
+        {
+          g_printerr ("The 'version' property is not available for '%s', "
+                      "either there isn't a working xdg-desktop-portal or "
+                      "it is a very old version\n", portal_interface_name[i]);
+
+          json_builder_add_boolean_value (builder, FALSE);
+          ret = EXIT_FAILURE;
+        }
+      else
+        {
+          json_builder_add_boolean_value (builder, TRUE);
+          json_builder_set_member_name (builder, "version");
+          version = g_variant_get_uint32 (variant_version);
+          json_builder_add_int_value (builder, version);
+        }
+      json_builder_end_object (builder);
+    }
+  json_builder_end_object (builder);
+
+  /* If we are in a Flatpak container we are not allowed to contact the
+   * portals implementations. So we just skip this test */
+  if (!g_file_test ("/.flatpak-info", G_FILE_TEST_IS_REGULAR))
+    {
+      json_builder_set_member_name (builder, "backends");
+      json_builder_begin_object (builder);
+      for (i = 0; portal_impl_name[i] != NULL; i++)
+        {
+          g_autoptr(GDBusProxy) portal_impl_proxy = NULL;
+          g_autofree gchar *impl_name_owner = NULL;
+          portal_impl_proxy = g_dbus_proxy_new_sync (connection,
+                                                     (G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                                      G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS),
+                                                     NULL,
+                                                     portal_impl_name[i],
+                                                     "/org/freedesktop/portal/desktop",
+                                                     "org.freedesktop.DBus.Peer",
+                                                     NULL,
+                                                     error);
+
+          json_builder_set_member_name (builder, portal_impl_name[i]);
+          json_builder_begin_object (builder);
+          json_builder_set_member_name (builder, "available");
+
+          if (portal_impl_proxy == NULL)
+            {
+              g_debug ("Failed to contact '%s': %s\n",
+                       portal_impl_name[i], local_error->message);
+              g_clear_error (error);
+              json_builder_add_boolean_value (builder, FALSE);
+              json_builder_end_object (builder);
+              continue;
+            }
+
+          impl_name_owner = g_dbus_proxy_get_name_owner (portal_impl_proxy);
+          if (impl_name_owner == NULL)
+            {
+              g_debug ("Failed to connect to '%s'\n", portal_impl_name[i]);
+              json_builder_add_boolean_value (builder, FALSE);
+            }
+          else
+            {
+              impl_available = TRUE;
+              json_builder_add_boolean_value (builder, TRUE);
+            }
+
+          json_builder_end_object (builder);
+        }
+      json_builder_end_object (builder);
+
+      /* We just need a single portal implementation to be available */
+      if (!impl_available)
+        {
+          g_printerr ("There isn't a working portal implementation\n");
+          ret = EXIT_FAILURE;
+        }
+    }
+
+  json_builder_end_object (builder);
+
+  root = json_builder_get_root (builder);
+  generator = json_generator_new ();
+  json_generator_set_pretty (generator, TRUE);
+  json_generator_set_root (generator, root);
+  json = json_generator_to_data (generator, NULL);
+  if (fputs (json, original_stdout) < 0)
+    g_warning ("Unable to write output: %s", g_strerror (errno));
+
+  if (fputs ("\n", original_stdout) < 0)
+    g_warning ("Unable to write final newline: %s", g_strerror (errno));
+
+  return ret;
+}
diff --git a/helpers/meson.build b/helpers/meson.build
index be8331aa4ca80d52fc169ab7752308901a1a01c8..b832a20e9be9252f728c12cc229fdfe2cbed3292 100644
--- a/helpers/meson.build
+++ b/helpers/meson.build
@@ -125,4 +125,17 @@ executable(
   )
 )
 
+executable(
+  multiarch + '-check-xdg-portal',
+  'check-xdg-portal.c',
+  include_directories : project_include_dirs,
+  dependencies : [glib, gio_unix, libglnx_dep, libsteamrt_dep, json_glib],
+  install : true,
+  install_dir : pkglibexecdir,
+  # Use the adjacent libsteam-runtime-tools and json-glib, ignoring
+  # LD_LIBRARY_PATH even if set
+  build_rpath : pkglibexec_rpath,
+  install_rpath : pkglibexec_rpath,
+)
+
 # vim:set sw=2 sts=2 et:
diff --git a/steam-runtime-tools/meson.build b/steam-runtime-tools/meson.build
index 7be80764574a2f8de0c227e6d6e440a136436aad..e710cc5e2b2b8348a9bcba44aeef03a66b9d84ed 100644
--- a/steam-runtime-tools/meson.build
+++ b/steam-runtime-tools/meson.build
@@ -47,6 +47,7 @@ libsteamrt_sources = [
     'system-info.c',
     'utils-internal.h',
     'utils.c',
+    'xdg-portal.c',
 ]
 
 libsteamrt_public_headers = [
@@ -62,6 +63,7 @@ libsteamrt_public_headers = [
     'steam.h',
     'system-info.h',
     'utils.h',
+    'xdg-portal.h',
 ]
 
 enums = gnome.mkenums_simple(
diff --git a/steam-runtime-tools/steam-runtime-tools.h b/steam-runtime-tools/steam-runtime-tools.h
index 244fe89558ba69ddc502f35dd061f97d25dd0788..29e02af44daeb3d28bf12958e8d6a828e884c2c3 100644
--- a/steam-runtime-tools/steam-runtime-tools.h
+++ b/steam-runtime-tools/steam-runtime-tools.h
@@ -39,5 +39,6 @@
 #include <steam-runtime-tools/steam.h>
 #include <steam-runtime-tools/system-info.h>
 #include <steam-runtime-tools/utils.h>
+#include <steam-runtime-tools/xdg-portal.h>
 
 #undef _SRT_IN_SINGLE_HEADER
diff --git a/steam-runtime-tools/system-info.c b/steam-runtime-tools/system-info.c
index a9996d41f82c5477a64fcf2d929681fc48991a49..346846e05ba31694802325633d1088c96b77afdb 100644
--- a/steam-runtime-tools/system-info.c
+++ b/steam-runtime-tools/system-info.c
@@ -25,11 +25,14 @@
 
 #include "steam-runtime-tools/system-info.h"
 
+/* Include this at the beginning so that every backports of
+ * G_DEFINE_AUTOPTR_CLEANUP_FUNC will be visible */
+#include "steam-runtime-tools/glib-backports-internal.h"
+
 #include "steam-runtime-tools/architecture.h"
 #include "steam-runtime-tools/architecture-internal.h"
 #include "steam-runtime-tools/cpu-feature-internal.h"
 #include "steam-runtime-tools/desktop-entry-internal.h"
-#include "steam-runtime-tools/glib-backports-internal.h"
 #include "steam-runtime-tools/graphics.h"
 #include "steam-runtime-tools/graphics-internal.h"
 #include "steam-runtime-tools/library-internal.h"
@@ -39,6 +42,7 @@
 #include "steam-runtime-tools/runtime-internal.h"
 #include "steam-runtime-tools/steam-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
+#include "steam-runtime-tools/xdg-portal-internal.h"
 
 #include <errno.h>
 #include <fcntl.h>
@@ -110,6 +114,7 @@ struct _SrtSystemInfo
   gboolean immutable_values;
   GHashTable *cached_hidden_deps;
   SrtSteam *steam_data;
+  SrtXdgPortal *xdg_portal_data;
   struct
   {
     /* GQuark => MaybeLocale */
@@ -467,6 +472,15 @@ forget_pinned_libs (SrtSystemInfo *self)
   self->pinned_libs.have_data = FALSE;
 }
 
+/*
+ * Forget any cached information about xdg portals.
+ */
+static void
+forget_xdg_portal (SrtSystemInfo *self)
+{
+  g_clear_object (&self->xdg_portal_data);
+}
+
 static void
 srt_system_info_finalize (GObject *object)
 {
@@ -481,6 +495,7 @@ srt_system_info_finalize (GObject *object)
   forget_pinned_libs (self);
   forget_runtime (self);
   forget_steam (self);
+  forget_xdg_portal (self);
 
   g_clear_pointer (&self->abis, g_ptr_array_unref);
   g_free (self->expectations);
@@ -733,6 +748,8 @@ srt_system_info_new_from_json (const char *path,
   info->desktop_entry.have_data = TRUE;
   info->desktop_entry.values = _srt_get_steam_desktop_entries_from_json_report (json_obj);
 
+  info->xdg_portal_data = _srt_xdg_portal_get_info_from_report (json_obj);
+
   info->cpu_features.x86_features = _srt_feature_get_x86_flags_from_report (json_obj,
                                                                             &info->cpu_features.x86_known);
 
@@ -1885,6 +1902,7 @@ srt_system_info_set_environ (SrtSystemInfo *self,
   forget_graphics_results (self);
   forget_locales (self);
   forget_pinned_libs (self);
+  forget_xdg_portal (self);
   g_clear_pointer (&self->cached_driver_environment, g_strfreev);
   g_strfreev (self->env);
 
@@ -3586,3 +3604,106 @@ srt_system_info_dup_steamscript_version (SrtSystemInfo *self)
   ensure_steam_cached (self);
   return g_strdup (srt_steam_get_steamscript_version (self->steam_data));
 }
+
+static void
+ensure_xdg_portals_cached (SrtSystemInfo *self)
+{
+  g_return_if_fail (SRT_IS_SYSTEM_INFO (self));
+
+  if (self->xdg_portal_data == NULL)
+    {
+      ensure_container_info (self);
+      _srt_check_xdg_portals (self->env,
+                              self->helpers_path,
+                              self->test_flags,
+                              self->container.type,
+                              srt_system_info_get_primary_multiarch_tuple (self),
+                              &self->xdg_portal_data);
+    }
+}
+
+/**
+ * srt_system_info_list_xdg_portal_backends:
+ * @self: The #SrtSystemInfo object
+ *
+ * List the XDG portal backends that have been checked, with information about
+ * their eventual availability.
+ *
+ * Examples of XDG portal backends are "org.freedesktop.impl.portal.desktop.gtk"
+ * and "org.freedesktop.impl.portal.desktop.kde".
+ *
+ * The returned list is in the same arbitrary order as the check-xdg-portal
+ * helper used to return them.
+ *
+ * Returns: (transfer full) (element-type SrtXdgPortalBackend) (nullable):
+ *  A list of opaque #SrtXdgPortalBackend objects or %NULL if an error
+ *  occurred trying to check the backends availability or if we were unable to
+ *  check them, e.g. if we are in a Flatpak environment.
+ *  Free with `g_list_free_full (backends, g_object_unref)`.
+ */
+GList *
+srt_system_info_list_xdg_portal_backends (SrtSystemInfo *self)
+{
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), NULL);
+
+  if (!self->immutable_values)
+    ensure_xdg_portals_cached (self);
+
+  return srt_xdg_portal_get_backends (self->xdg_portal_data);
+}
+
+/**
+ * srt_system_info_list_xdg_portal_interfaces:
+ * @self: The #SrtSystemInfo object
+ *
+ * List the XDG portal interfaces that have been checked, with information
+ * about their eventual availability and version property.
+ *
+ * Examples of XDG portal interfaces are "org.freedesktop.portal.OpenURI"
+ * and "org.freedesktop.portal.Email".
+ *
+ * The returned list is in the same arbitrary order as the check-xdg-portal
+ * helper used to return them.
+ *
+ * Returns: (transfer full) (element-type SrtXdgPortalInterface) (nullable):
+ *  A list of opaque #SrtXdgPortalInterface objects or %NULL if an error
+ *  occurred trying to check the interfaces availability. Free with
+ *  `g_list_free_full (interfaces, g_object_unref)`.
+ */
+GList *
+srt_system_info_list_xdg_portal_interfaces (SrtSystemInfo *self)
+{
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), NULL);
+
+  if (!self->immutable_values)
+    ensure_xdg_portals_cached (self);
+
+  return srt_xdg_portal_get_interfaces (self->xdg_portal_data);
+}
+
+/**
+ * srt_system_info_get_xdg_portal_issues:
+ * @self: The #SrtSystemInfo object
+ * @messages: (optional) (out): If not %NULL, used to return the diagnostic
+ *  messages. Free with g_free().
+ *
+ * Check if the current system supports the XDG portals.
+ *
+ * Returns: A bitfield containing problems, or %SRT_XDG_PORTAL_ISSUES_NONE
+ *  if no problems were found.
+ */
+SrtXdgPortalIssues
+srt_system_info_get_xdg_portal_issues (SrtSystemInfo *self,
+                                       gchar **messages)
+{
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+  g_return_val_if_fail (messages == NULL || *messages == NULL, SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+
+  if (!self->immutable_values)
+    ensure_xdg_portals_cached (self);
+
+  if (messages != NULL)
+    *messages = g_strdup (srt_xdg_portal_get_messages (self->xdg_portal_data));
+
+  return srt_xdg_portal_get_issues (self->xdg_portal_data);
+}
diff --git a/steam-runtime-tools/system-info.h b/steam-runtime-tools/system-info.h
index 0c064004c3eab15f1855b3d0b26f3bd456080b0d..654759f89cc66eedbd407c2833657abe5fa754e5 100644
--- a/steam-runtime-tools/system-info.h
+++ b/steam-runtime-tools/system-info.h
@@ -39,6 +39,7 @@
 #include <steam-runtime-tools/macros.h>
 #include <steam-runtime-tools/runtime.h>
 #include <steam-runtime-tools/steam.h>
+#include <steam-runtime-tools/xdg-portal.h>
 
 /**
  * SrtTestFlags:
@@ -262,6 +263,14 @@ gchar *srt_system_info_dup_steamscript_path (SrtSystemInfo *self);
 _SRT_PUBLIC
 gchar *srt_system_info_dup_steamscript_version (SrtSystemInfo *self);
 
+_SRT_PUBLIC
+GList *srt_system_info_list_xdg_portal_backends (SrtSystemInfo *self);
+_SRT_PUBLIC
+GList *srt_system_info_list_xdg_portal_interfaces (SrtSystemInfo *self);
+_SRT_PUBLIC
+SrtXdgPortalIssues srt_system_info_get_xdg_portal_issues (SrtSystemInfo *self,
+                                                          gchar **messages);
+
 #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtSystemInfo, g_object_unref)
 #endif
diff --git a/steam-runtime-tools/xdg-portal-internal.h b/steam-runtime-tools/xdg-portal-internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..3eeddcfa5ba98de74696a7171ed9bcd661f7db3d
--- /dev/null
+++ b/steam-runtime-tools/xdg-portal-internal.h
@@ -0,0 +1,126 @@
+/*< internal_header >*/
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#pragma once
+
+#include "steam-runtime-tools/steam-runtime-tools.h"
+
+#include <json-glib/json-glib.h>
+
+#ifndef __GTK_DOC_IGNORE__
+static inline SrtXdgPortalInterface *_srt_xdg_portal_interface_new (const char *name,
+                                                                    gboolean is_available,
+                                                                    guint version);
+
+static inline SrtXdgPortalInterface *
+_srt_xdg_portal_interface_new (const char *name,
+                               gboolean is_available,
+                               guint version)
+{
+  g_return_val_if_fail (name != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_XDG_PORTAL_INTERFACE,
+                       "name", name,
+                       "is-available", is_available,
+                       "version", version,
+                       NULL);
+}
+
+static inline SrtXdgPortalBackend *_srt_xdg_portal_backend_new (const char *name,
+                                                                gboolean is_available);
+
+static inline SrtXdgPortalBackend *
+_srt_xdg_portal_backend_new (const char *name,
+                             gboolean is_available)
+{
+  g_return_val_if_fail (name != NULL, NULL);
+
+  return g_object_new (SRT_TYPE_XDG_PORTAL_BACKEND,
+                       "name", name,
+                       "is-available", is_available,
+                       NULL);
+}
+
+typedef struct _SrtXdgPortal SrtXdgPortal;
+typedef struct _SrtXdgPortalClass SrtXdgPortalClass;
+
+#define SRT_TYPE_XDG_PORTAL srt_xdg_portal_get_type ()
+#define SRT_XDG_PORTAL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SRT_TYPE_XDG_PORTAL, SrtXdgPortal))
+#define SRT_XDG_PORTAL_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), SRT_TYPE_XDG_PORTAL, SrtXdgPortalClass))
+#define SRT_IS_XDG_PORTAL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SRT_TYPE_XDG_PORTAL))
+#define SRT_IS_XDG_PORTAL_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), SRT_TYPE_XDG_PORTAL))
+#define SRT_XDG_PORTAL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SRT_TYPE_XDG_PORTAL, SrtXdgPortalClass)
+
+G_GNUC_INTERNAL
+GType srt_xdg_portal_get_type (void);
+
+/*
+ * @backends: (transfer none) (element-type SrtXdgPortalBackend):
+ * @interfaces: (transfer none) (element-type SrtXdgPortalInterface):
+ */
+static inline SrtXdgPortal *_srt_xdg_portal_new (const char *messages,
+                                                 SrtXdgPortalIssues issues,
+                                                 GPtrArray *backends,
+                                                 GPtrArray *interfaces);
+
+static inline SrtXdgPortal *
+_srt_xdg_portal_new (const char *messages,
+                     SrtXdgPortalIssues issues,
+                     GPtrArray *backends,
+                     GPtrArray *interfaces)
+{
+  return g_object_new (SRT_TYPE_XDG_PORTAL,
+                       "messages", messages,
+                       "issues", issues,
+                       "backends", backends,
+                       "interfaces", interfaces,
+                       NULL);
+}
+
+G_GNUC_INTERNAL
+SrtXdgPortalIssues _srt_check_xdg_portals (gchar **envp,
+                                           const char *helpers_path,
+                                           SrtTestFlags test_flags,
+                                           SrtContainerType container_type,
+                                           const char *multiarch_tuple,
+                                           SrtXdgPortal **details_out);
+#endif
+
+G_GNUC_INTERNAL
+const char *srt_xdg_portal_get_messages (SrtXdgPortal *self);
+G_GNUC_INTERNAL
+SrtXdgPortalIssues srt_xdg_portal_get_issues (SrtXdgPortal *self);
+G_GNUC_INTERNAL
+GList *srt_xdg_portal_get_backends (SrtXdgPortal *self);
+G_GNUC_INTERNAL
+GList *srt_xdg_portal_get_interfaces (SrtXdgPortal *self);
+
+G_GNUC_INTERNAL
+SrtXdgPortal *_srt_xdg_portal_get_info_from_report (JsonObject *json_obj);
+
+#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtXdgPortal, g_object_unref)
+#endif
diff --git a/steam-runtime-tools/xdg-portal.c b/steam-runtime-tools/xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..ba4c7af2ed8f4ea20b1e3298712232422655c735
--- /dev/null
+++ b/steam-runtime-tools/xdg-portal.c
@@ -0,0 +1,944 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "steam-runtime-tools/xdg-portal.h"
+
+#include "steam-runtime-tools/enums.h"
+#include "steam-runtime-tools/glib-backports-internal.h"
+#include "steam-runtime-tools/json-glib-compat.h"
+#include "steam-runtime-tools/xdg-portal-internal.h"
+#include "steam-runtime-tools/utils.h"
+#include "steam-runtime-tools/utils-internal.h"
+
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include <json-glib/json-glib.h>
+
+/**
+ * SECTION:xdg-portal
+ * @title: XDG portals support check
+ * @short_description: Get information about system's XDG portals support
+ * @include: steam-runtime-tools/steam-runtime-tools.h
+ *
+ * #SrtXdgPortal is an opaque object representing the XDG portals support.
+ * This is a reference-counted object: use g_object_ref() and
+ * g_object_unref() to manage its lifecycle.
+ *
+ * #SrtXdgPortalBackend is an opaque object representing an XDG portal
+ * backend. This is a reference-counted object: use g_object_ref() and
+ * g_object_unref() to manage its lifecycle.
+ *
+ * #SrtXdgPortalInterface is an opaque object representing an XDG portal
+ * inerface. This is a reference-counted object: use g_object_ref() and
+ * g_object_unref() to manage its lifecycle.
+ */
+
+struct _SrtXdgPortalInterface
+{
+  /*< private >*/
+  GObject parent;
+  gchar *name;
+  gboolean available;
+  guint version;
+};
+
+struct _SrtXdgPortalInterfaceClass
+{
+  /*< private >*/
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_INTERFACE_0,
+  PROP_INTERFACE_NAME,
+  PROP_INTERFACE_VERSION,
+  PROP_INTERFACE_IS_AVAILABLE,
+  N_INTERFACE_PROPERTIES,
+};
+
+G_DEFINE_TYPE (SrtXdgPortalInterface, srt_xdg_portal_interface, G_TYPE_OBJECT)
+
+static void
+srt_xdg_portal_interface_init (SrtXdgPortalInterface *self)
+{
+}
+
+static void
+srt_xdg_portal_interface_get_property (GObject *object,
+                                       guint prop_id,
+                                       GValue *value,
+                                       GParamSpec *pspec)
+{
+  SrtXdgPortalInterface *self = SRT_XDG_PORTAL_INTERFACE (object);
+
+  switch (prop_id)
+    {
+      case PROP_INTERFACE_NAME:
+        g_value_set_string (value, self->name);
+        break;
+
+      case PROP_INTERFACE_VERSION:
+        g_value_set_uint (value, self->version);
+        break;
+
+      case PROP_INTERFACE_IS_AVAILABLE:
+        g_value_set_boolean (value, self->available);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_interface_set_property (GObject *object,
+                                       guint prop_id,
+                                       const GValue *value,
+                                       GParamSpec *pspec)
+{
+  SrtXdgPortalInterface *self = SRT_XDG_PORTAL_INTERFACE (object);
+
+  switch (prop_id)
+    {
+      case PROP_INTERFACE_NAME:
+        /* Construct-only */
+        g_return_if_fail (self->name == NULL);
+        self->name = g_value_dup_string (value);
+        break;
+
+      case PROP_INTERFACE_VERSION:
+        /* Construct-only */
+        g_return_if_fail (self->version == 0);
+        self->version = g_value_get_uint (value);
+        break;
+
+      case PROP_INTERFACE_IS_AVAILABLE:
+        g_return_if_fail (self->available == FALSE);
+        self->available = g_value_get_boolean (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_interface_finalize (GObject *object)
+{
+  SrtXdgPortalInterface *self = SRT_XDG_PORTAL_INTERFACE (object);
+
+  g_free (self->name);
+
+  G_OBJECT_CLASS (srt_xdg_portal_interface_parent_class)->finalize (object);
+}
+
+static GParamSpec *interface_properties[N_INTERFACE_PROPERTIES] = { NULL };
+
+static void
+srt_xdg_portal_interface_class_init (SrtXdgPortalInterfaceClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_xdg_portal_interface_get_property;
+  object_class->set_property = srt_xdg_portal_interface_set_property;
+  object_class->finalize = srt_xdg_portal_interface_finalize;
+
+  interface_properties[PROP_INTERFACE_NAME] =
+    g_param_spec_string ("name", "Name",
+                         "Name of this XDG portal interface, e.g. 'org.freedesktop.portal.Email'",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  interface_properties[PROP_INTERFACE_VERSION] =
+    g_param_spec_uint ("version", "Version",
+                       "The version property of this XDG portal interface",
+                       0,
+                       G_MAXUINT32,
+                       0,
+                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                       G_PARAM_STATIC_STRINGS);
+
+  interface_properties[PROP_INTERFACE_IS_AVAILABLE] =
+    g_param_spec_boolean ("is-available", "Is available?",
+                          "TRUE if this XDG portal is available",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_INTERFACE_PROPERTIES, interface_properties);
+}
+
+struct _SrtXdgPortalBackend
+{
+  /*< private >*/
+  GObject parent;
+  gchar *name;
+  gboolean available;
+};
+
+struct _SrtXdgPortalBackendClass
+{
+  /*< private >*/
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_BACKEND_0,
+  PROP_BACKEND_NAME,
+  PROP_BACKEND_IS_AVAILABLE,
+  N_PROP_BACKEND_PROPERTIES,
+};
+
+G_DEFINE_TYPE (SrtXdgPortalBackend, srt_xdg_portal_backend, G_TYPE_OBJECT)
+
+static void
+srt_xdg_portal_backend_init (SrtXdgPortalBackend *self)
+{
+}
+
+static void
+srt_xdg_portal_backend_get_property (GObject *object,
+                                     guint prop_id,
+                                     GValue *value,
+                                     GParamSpec *pspec)
+{
+  SrtXdgPortalBackend *self = SRT_XDG_PORTAL_BACKEND (object);
+
+  switch (prop_id)
+    {
+      case PROP_BACKEND_NAME:
+        g_value_set_string (value, self->name);
+        break;
+
+      case PROP_BACKEND_IS_AVAILABLE:
+        g_value_set_boolean (value, self->available);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_backend_set_property (GObject *object,
+                                     guint prop_id,
+                                     const GValue *value,
+                                     GParamSpec *pspec)
+{
+  SrtXdgPortalBackend *self = SRT_XDG_PORTAL_BACKEND (object);
+
+  switch (prop_id)
+    {
+      case PROP_BACKEND_NAME:
+        /* Construct-only */
+        g_return_if_fail (self->name == NULL);
+        self->name = g_value_dup_string (value);
+        break;
+
+      case PROP_BACKEND_IS_AVAILABLE:
+        g_return_if_fail (self->available == FALSE);
+        self->available = g_value_get_boolean (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_backend_finalize (GObject *object)
+{
+  SrtXdgPortalBackend *self = SRT_XDG_PORTAL_BACKEND (object);
+
+  g_free (self->name);
+
+  G_OBJECT_CLASS (srt_xdg_portal_backend_parent_class)->finalize (object);
+}
+
+static GParamSpec *backend_properties[N_PROP_BACKEND_PROPERTIES] = { NULL };
+
+static void
+srt_xdg_portal_backend_class_init (SrtXdgPortalBackendClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_xdg_portal_backend_get_property;
+  object_class->set_property = srt_xdg_portal_backend_set_property;
+  object_class->finalize = srt_xdg_portal_backend_finalize;
+
+  backend_properties[PROP_BACKEND_NAME] =
+    g_param_spec_string ("name", "Name",
+                         "Name of this XDG portal backend, e.g. 'org.freedesktop.impl.portal.desktop.gtk'",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  backend_properties[PROP_BACKEND_IS_AVAILABLE] =
+    g_param_spec_boolean ("is-available", "Is available?",
+                          "TRUE if this XDG portal is available",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROP_BACKEND_PROPERTIES, backend_properties);
+}
+
+struct _SrtXdgPortal
+{
+  /*< private >*/
+  GObject parent;
+  gchar *messages;
+  SrtXdgPortalIssues issues;
+  GPtrArray *portals_backends;
+  GPtrArray *portals_interfaces;
+};
+
+struct _SrtXdgPortalClass
+{
+  /*< private >*/
+  GObjectClass parent_class;
+};
+
+enum {
+  PROP_PORTAL_0,
+  PROP_PORTAL_MESSAGES,
+  PROP_PORTAL_ISSUES,
+  PROP_PORTAL_BACKENDS,
+  PROP_PORTAL_INTERFACES,
+  N_PORTAL_PROPERTIES,
+};
+
+G_DEFINE_TYPE (SrtXdgPortal, srt_xdg_portal, G_TYPE_OBJECT)
+
+static void
+srt_xdg_portal_init (SrtXdgPortal *self)
+{
+}
+
+static void
+srt_xdg_portal_get_property (GObject *object,
+                             guint prop_id,
+                             GValue *value,
+                             GParamSpec *pspec)
+{
+  SrtXdgPortal *self = SRT_XDG_PORTAL (object);
+
+  switch (prop_id)
+    {
+      case PROP_PORTAL_MESSAGES:
+        g_value_set_string (value, self->messages);
+        break;
+
+      case PROP_PORTAL_ISSUES:
+        g_value_set_flags (value, self->issues);
+        break;
+
+      case PROP_PORTAL_BACKENDS:
+        g_value_set_boxed (value, self->portals_backends);
+        break;
+
+      case PROP_PORTAL_INTERFACES:
+        g_value_set_boxed (value, self->portals_interfaces);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_set_property (GObject *object,
+                             guint prop_id,
+                             const GValue *value,
+                             GParamSpec *pspec)
+{
+  SrtXdgPortal *self = SRT_XDG_PORTAL (object);
+
+  switch (prop_id)
+    {
+      case PROP_PORTAL_MESSAGES:
+        /* Construct-only */
+        g_return_if_fail (self->messages == NULL);
+        self->messages = g_value_dup_string (value);
+        break;
+
+      case PROP_PORTAL_ISSUES:
+        /* Construct-only */
+        g_return_if_fail (self->issues == 0);
+        self->issues = g_value_get_flags (value);
+        break;
+
+      case PROP_PORTAL_BACKENDS:
+        /* Construct-only */
+        g_return_if_fail (self->portals_backends == NULL);
+        self->portals_backends = g_value_dup_boxed (value);
+        break;
+
+      case PROP_PORTAL_INTERFACES:
+        /* Construct-only */
+        g_return_if_fail (self->portals_interfaces == NULL);
+        self->portals_interfaces = g_value_dup_boxed (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+srt_xdg_portal_dispose (GObject *object)
+{
+  SrtXdgPortal *self = SRT_XDG_PORTAL (object);
+
+  g_clear_pointer (&self->portals_backends, g_ptr_array_unref);
+  g_clear_pointer (&self->portals_interfaces, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (srt_xdg_portal_parent_class)->dispose (object);
+}
+
+static void
+srt_xdg_portal_finalize (GObject *object)
+{
+  SrtXdgPortal *self = SRT_XDG_PORTAL (object);
+
+  g_free (self->messages);
+
+  G_OBJECT_CLASS (srt_xdg_portal_parent_class)->finalize (object);
+}
+
+static GParamSpec *properties[N_PORTAL_PROPERTIES] = { NULL };
+
+static void
+srt_xdg_portal_class_init (SrtXdgPortalClass *cls)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+  object_class->get_property = srt_xdg_portal_get_property;
+  object_class->set_property = srt_xdg_portal_set_property;
+  object_class->dispose = srt_xdg_portal_dispose;
+  object_class->finalize = srt_xdg_portal_finalize;
+
+  properties[PROP_PORTAL_MESSAGES] =
+    g_param_spec_string ("messages", "Messages",
+                         "Diagnostic messages produced while checking XDG portals support",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  properties[PROP_PORTAL_ISSUES] =
+    g_param_spec_flags ("issues", "Issues", "Problems with the XDG portals",
+                        SRT_TYPE_XDG_PORTAL_ISSUES, SRT_XDG_PORTAL_ISSUES_NONE,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS);
+
+  properties[PROP_PORTAL_BACKENDS] =
+    g_param_spec_boxed ("backends", "Backends",
+                        "List of XDG portals backends that have been checked",
+                        G_TYPE_PTR_ARRAY,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS);
+
+  properties[PROP_PORTAL_INTERFACES] =
+    g_param_spec_boxed ("interfaces", "Interfaces",
+                        "List of XDG portals interfaces that have been checked",
+                        G_TYPE_PTR_ARRAY,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PORTAL_PROPERTIES, properties);
+}
+
+/**
+ * _srt_check_xdg_portals:
+ * @envp: (not nullable): Environment variables to use
+ * @helpers_path: An optional path to find check-xdg-portal helper, PATH
+ *  is used if null.
+ * @test_flags: Flags used during automated testing
+ * @multiarch_tuple: Multiarch tuple of helper executable to use
+ * @details_out: The SrtXdgPortal object containing the details of the check.
+ *
+ * Return the problems found when checking the available XDG portals.
+ *
+ * Returns: A bitfield containing problems, or %SRT_XDG_PORTAL_ISSUES_NONE
+ *  if no problems were found
+ */
+G_GNUC_INTERNAL SrtXdgPortalIssues
+_srt_check_xdg_portals (gchar **envp,
+                        const char *helpers_path,
+                        SrtTestFlags test_flags,
+                        SrtContainerType container_type,
+                        const char *multiarch_tuple,
+                        SrtXdgPortal **details_out)
+{
+  g_autoptr(GError) local_error = NULL;
+  SrtHelperFlags helper_flags = (SRT_HELPER_FLAGS_TIME_OUT
+                                 | SRT_HELPER_FLAGS_SEARCH_PATH);
+  g_autoptr(GPtrArray) argv = NULL;
+  g_autofree gchar *output = NULL;
+  g_autofree gchar *stderr_messages = NULL;
+  int wait_status = -1;
+  int exit_status = -1;
+  int terminating_signal = 0;
+  JsonObject *object = NULL;
+  JsonObject *interfaces_object = NULL;
+  JsonObject *backends_object = NULL;
+  JsonNode *node = NULL;
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GList) interfaces_members = NULL;
+  g_autoptr(GList) backends_members = NULL;
+  g_autoptr(GPtrArray) backends = g_ptr_array_new_full (2, g_object_unref);
+  g_autoptr(GPtrArray) interfaces = g_ptr_array_new_full (2, g_object_unref);
+  SrtXdgPortalIssues issues = SRT_XDG_PORTAL_ISSUES_NONE;
+  gboolean has_implementation = FALSE;
+
+  g_return_val_if_fail (envp != NULL, SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+  g_return_val_if_fail (multiarch_tuple != NULL, SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+  g_return_val_if_fail (details_out == NULL || *details_out == NULL,
+                        SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+
+  if (test_flags & SRT_TEST_FLAGS_TIME_OUT_SOONER)
+    helper_flags |= SRT_HELPER_FLAGS_TIME_OUT_SOONER;
+
+  argv = _srt_get_helper (helpers_path, multiarch_tuple, "check-xdg-portal",
+                          helper_flags, &local_error);
+
+  if (argv == NULL)
+    {
+      g_debug ("An error occurred trying to check the D-Bus portals capabilities: %s",
+               local_error->message);
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (local_error->message, issues, NULL, NULL);
+      return issues;
+    }
+
+  /* NULL terminate the array */
+  g_ptr_array_add (argv, NULL);
+
+  if (!g_spawn_sync (NULL,    /* working directory */
+                     (gchar **) argv->pdata,
+                     envp,
+                     G_SPAWN_SEARCH_PATH,
+                     _srt_child_setup_unblock_signals,
+                     NULL,    /* user data */
+                     &output, /* stdout */
+                     &stderr_messages,
+                     &wait_status,
+                     &local_error))
+    {
+      g_debug ("An error occurred calling the helper: %s", local_error->message);
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (local_error->message, issues, NULL, NULL);
+      return issues;
+    }
+
+  /* Normalize the empty string (expected to be common) to NULL */
+  if (stderr_messages != NULL && stderr_messages[0] == '\0')
+    g_clear_pointer (&stderr_messages, g_free);
+
+  if (wait_status != 0)
+    {
+      g_debug ("... wait status %d", wait_status);
+      if (_srt_process_timeout_wait_status (wait_status, &exit_status, &terminating_signal))
+        {
+          issues |= SRT_XDG_PORTAL_ISSUES_TIMEOUT;
+          if (details_out != NULL)
+            *details_out = _srt_xdg_portal_new (NULL, issues, NULL, NULL);
+          return issues;
+        }
+    }
+  else
+    {
+      exit_status = 0;
+    }
+
+  if (exit_status == 1)
+    g_debug ("The helper exited with 1, either a required xdg portal is missing or an error occurred");
+
+  if (output == NULL || output[0] == '\0')
+    {
+      g_print ("The helper exited without printing the expected JSON in output");
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (stderr_messages, issues, NULL, NULL);
+      return issues;
+    }
+
+  /* We can't use `json_from_string()` directly because we are targeting an
+   * older json-glib version */
+  parser = json_parser_new ();
+
+  if (!json_parser_load_from_data (parser, output, -1, &local_error))
+    {
+      g_debug ("The helper output is not a valid JSON: %s", local_error->message);
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (local_error->message, issues, NULL, NULL);
+      return issues;
+    }
+
+  node = json_parser_get_root (parser);
+
+  if (node == NULL || !JSON_NODE_HOLDS_OBJECT (node))
+    {
+      g_debug ("The helper output is not a JSON object");
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (local_error->message, issues, NULL, NULL);
+      return issues;
+    }
+
+  object = json_node_get_object (node);
+
+  if (!json_object_has_member (object, "interfaces"))
+    {
+      g_debug ("The helper output JSON is malformed or incomplete");
+      issues |= SRT_XDG_PORTAL_ISSUES_UNKNOWN;
+      if (details_out != NULL)
+        *details_out = _srt_xdg_portal_new (local_error->message, issues, NULL, NULL);
+      return issues;
+    }
+
+  interfaces_object = json_object_get_object_member (object, "interfaces");
+  interfaces_members = json_object_get_members (interfaces_object);
+  for (const GList *l = interfaces_members; l != NULL; l = l->next)
+    {
+      gboolean available = FALSE;
+      guint version = 0;
+      JsonObject *interface_object = NULL;
+      interface_object = json_object_get_object_member (interfaces_object, l->data);
+      if (json_object_has_member (interface_object, "available"))
+        available = json_object_get_boolean_member (interface_object, "available");
+      else
+        g_debug ("The helper output JSON is missing the 'available' field, we assume it to be 'FALSE'");
+      if (available)
+        {
+          if (json_object_has_member (interface_object, "version"))
+            version = json_object_get_int_member (interface_object, "version");
+          else
+            g_debug ("The helper output JSON is missing the 'version' field, we assume it to be '0'");
+        }
+      else
+        {
+          issues |= SRT_XDG_PORTAL_ISSUES_MISSING_INTERFACE;
+        }
+
+      if (details_out != NULL)
+        g_ptr_array_add (interfaces,
+                         _srt_xdg_portal_interface_new (l->data, available, version));
+    }
+
+  if (interfaces_members == NULL)
+    issues |= SRT_XDG_PORTAL_ISSUES_MISSING_INTERFACE;
+
+  /* If 'backends' is missing is not necessarily an error */
+  if (json_object_has_member (object, "backends"))
+    {
+      backends_object = json_object_get_object_member (object, "backends");
+      backends_members = json_object_get_members (backends_object);
+      for (const GList *l = backends_members; l != NULL; l = l->next)
+        {
+          gboolean available = FALSE;
+          JsonObject *backend_object = NULL;
+          backend_object = json_object_get_object_member (backends_object, l->data);
+          if (json_object_has_member (backend_object, "available"))
+            available = json_object_get_boolean_member (backend_object, "available");
+          else
+            g_debug ("The helper output JSON is missing the 'available' field, we assume it to be 'FALSE'");
+
+          if (available)
+            has_implementation = TRUE;
+
+          if (details_out != NULL)
+            g_ptr_array_add (backends,
+                             _srt_xdg_portal_backend_new (l->data, available));
+        }
+    }
+
+  if (container_type == SRT_CONTAINER_TYPE_FLATPAK)
+    {
+      g_debug ("In a Flatpak container we are not allowed to contact the portals implementations");
+    }
+  else if (!has_implementation)
+    {
+      issues |= SRT_XDG_PORTAL_ISSUES_NO_IMPLEMENTATION;
+    }
+
+  if (details_out != NULL)
+    {
+      *details_out = _srt_xdg_portal_new (stderr_messages,
+                                          issues,
+                                          backends,
+                                          interfaces);
+    }
+
+  return issues;
+}
+
+/**
+ * _srt_xdg_portal_get_info_from_report:
+ * @json_obj: (not nullable): A JSON Object used to search for the XDG portals info
+ *
+ * Returns: A new #SrtXdgPortal.
+ */
+SrtXdgPortal *
+_srt_xdg_portal_get_info_from_report (JsonObject *json_obj)
+{
+  JsonObject *json_portals_obj = NULL;
+  JsonObject *json_details_obj = NULL;
+  JsonObject *json_interfaces_obj = NULL;
+  JsonObject *json_backends_obj = NULL;
+  g_autoptr(GList) interfaces_members = NULL;
+  g_autoptr(GList) backends_members = NULL;
+  g_autoptr(GPtrArray) backends = g_ptr_array_new_full (2, g_object_unref);
+  g_autoptr(GPtrArray) interfaces = g_ptr_array_new_full (2, g_object_unref);
+  SrtXdgPortalIssues issues = SRT_XDG_PORTAL_ISSUES_NONE;
+  const gchar *messages = NULL;
+
+  g_return_val_if_fail (json_obj != NULL, NULL);
+
+  if (!json_object_has_member (json_obj, "xdg-portals"))
+    {
+      g_debug ("'xdg-portals' entry is missing");
+      return _srt_xdg_portal_new (NULL, SRT_XDG_PORTAL_ISSUES_UNKNOWN, NULL, NULL);
+    }
+
+  json_portals_obj = json_object_get_object_member (json_obj, "xdg-portals");
+
+  issues = srt_get_flags_from_json_array (SRT_TYPE_XDG_PORTAL_ISSUES,
+                                          json_portals_obj,
+                                          "issues",
+                                          SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+
+  if (json_object_has_member (json_portals_obj, "messages"))
+    messages = json_object_get_string_member (json_portals_obj, "messages");
+
+  if (!json_object_has_member (json_portals_obj, "details"))
+    return _srt_xdg_portal_new (messages, issues, NULL, NULL);
+
+  json_details_obj = json_object_get_object_member (json_portals_obj, "details");
+
+  if (json_object_has_member (json_details_obj, "interfaces"))
+    {
+      json_interfaces_obj = json_object_get_object_member (json_details_obj, "interfaces");
+      interfaces_members = json_object_get_members (json_interfaces_obj);
+      for (const GList *l = interfaces_members; l != NULL; l = l->next)
+        {
+          gboolean available = FALSE;
+          gint64 version = 0;
+          JsonObject *json_portal_obj = json_object_get_object_member (json_interfaces_obj, l->data);
+          if (json_object_has_member (json_portal_obj, "available"))
+            available = json_object_get_boolean_member (json_portal_obj, "available");
+
+          if (json_object_has_member (json_portal_obj, "version"))
+            version = json_object_get_int_member (json_portal_obj, "version");
+
+          g_ptr_array_add (interfaces,
+                           _srt_xdg_portal_interface_new (l->data, available, version));
+        }
+    }
+
+  if (json_object_has_member (json_details_obj, "backends"))
+    {
+      json_backends_obj = json_object_get_object_member (json_details_obj, "backends");
+      backends_members = json_object_get_members (json_backends_obj);
+      for (const GList *l = backends_members; l != NULL; l = l->next)
+        {
+          gboolean available = FALSE;
+          JsonObject *json_portal_obj = json_object_get_object_member (json_backends_obj, l->data);
+          if (json_object_has_member (json_portal_obj, "available"))
+            available = json_object_get_boolean_member (json_portal_obj, "available");
+
+          g_ptr_array_add (backends, _srt_xdg_portal_backend_new (l->data, available));
+        }
+    }
+
+  return _srt_xdg_portal_new (messages,
+                              issues,
+                              backends,
+                              interfaces);
+}
+
+/**
+ * srt_xdg_portal_get_messages:
+ * @self: an XDG portal object
+ *
+ * Return the diagnostic messages shown by the XDG portal check, or %NULL if
+ * none. The returned pointer is valid as long as a reference to @self is held.
+ *
+ * Returns: (nullable) (transfer none): #SrtXdgPortal:messages
+ */
+const char *
+srt_xdg_portal_get_messages (SrtXdgPortal *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL (self), NULL);
+  return self->messages;
+}
+
+/**
+ * srt_xdg_portal_get_issues:
+ * @self: an XDG portal object
+ *
+ * Return flags indicating issues found while checking for XDG desktop portals.
+ *
+ * Returns: #SrtXdgPortal:issues
+ */
+SrtXdgPortalIssues
+srt_xdg_portal_get_issues (SrtXdgPortal *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL (self), SRT_XDG_PORTAL_ISSUES_UNKNOWN);
+  return self->issues;
+}
+
+/**
+ * srt_xdg_portal_get_backends:
+ * @self: an XDG portal object
+ *
+ * Return the list of XDG portals backends that have been checked.
+ *
+ * Returns: (transfer full) (element-type SrtXdgPortalBackend): The backends.
+ *  Free with `g_list_free_full (list, g_object_unref)`.
+ */
+GList *
+srt_xdg_portal_get_backends (SrtXdgPortal *self)
+{
+  GList *ret = NULL;
+  guint i;
+
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL (self), NULL);
+
+  for (i = 0; self->portals_backends != NULL && i < self->portals_backends->len; i++)
+    ret = g_list_prepend (ret, g_object_ref (g_ptr_array_index (self->portals_backends, i)));
+
+  return g_list_reverse (ret);
+}
+
+/**
+ * srt_xdg_portal_get_interfaces:
+ * @self: an XDG portal object
+ *
+ * Return the list of XDG portals interfaces that have been checked.
+ *
+ * Returns: (transfer full) (element-type SrtXdgPortalInterface): The interfaces.
+ *  Free with `g_list_free_full (list, g_object_unref)`.
+ */
+GList *
+srt_xdg_portal_get_interfaces (SrtXdgPortal *self)
+{
+  GList *ret = NULL;
+  guint i;
+
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL (self), NULL);
+
+  for (i = 0; self->portals_interfaces != NULL && i < self->portals_interfaces->len; i++)
+    ret = g_list_prepend (ret, g_object_ref (g_ptr_array_index (self->portals_interfaces, i)));
+
+  return g_list_reverse (ret);
+}
+
+/**
+ * srt_xdg_portal_backend_get_name:
+ * @self: an XDG portal backend object
+ *
+ * Return the name of the XDG portal backend, for example
+ * `org.freedesktop.impl.portal.desktop.gtk` for the GTK/GNOME
+ * implementation. The returned string remains valid as long as
+ * a reference to @self is held.
+ *
+ * Returns: (transfer none) (not nullable): #SrtXdgPortalBackend:name
+ */
+const char *
+srt_xdg_portal_backend_get_name (SrtXdgPortalBackend *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL_BACKEND (self), NULL);
+  return self->name;
+}
+
+/**
+ * srt_xdg_portal_backend_is_available:
+ * @self: an XDG portal backend object
+ *
+ * Return %TRUE if the XDG portal backend is available.
+ *
+ * Returns: #SrtXdgPortalBackend:is-available
+ */
+gboolean
+srt_xdg_portal_backend_is_available (SrtXdgPortalBackend *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL_BACKEND (self), FALSE);
+  return self->available;
+}
+
+/**
+ * srt_xdg_portal_interface_get_name:
+ * @self: an XDG portal interface object
+ *
+ * Return the name of the XDG portal interface, for example
+ * `org.freedesktop.portal.Email`. The returned string
+ * remains valid as long as a reference to @self is held.
+ *
+ * Returns: (transfer none) (not nullable): #SrtXdgPortalInterface:name
+ */
+const char *
+srt_xdg_portal_interface_get_name (SrtXdgPortalInterface *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL_INTERFACE (self), NULL);
+  return self->name;
+}
+
+/**
+ * srt_xdg_portal_interface_is_available:
+ * @self: an XDG portal interface object
+ *
+ * Return %TRUE if the XDG portal interface is available.
+ *
+ * Returns: #SrtXdgPortalInterface:is-available
+ */
+gboolean
+srt_xdg_portal_interface_is_available (SrtXdgPortalInterface *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL_INTERFACE (self), FALSE);
+  return self->available;
+}
+
+/**
+ * srt_xdg_portal_interface_get_version:
+ * @self: an XDG portal interface object
+ *
+ * Return the version property of the XDG portal interface,
+ * or 0 if unknown or unavailable.
+ *
+ * Returns: #SrtXdgPortalInterface:version
+ */
+guint32
+srt_xdg_portal_interface_get_version (SrtXdgPortalInterface *self)
+{
+  g_return_val_if_fail (SRT_IS_XDG_PORTAL_INTERFACE (self), 0);
+  return self->version;
+}
diff --git a/steam-runtime-tools/xdg-portal.h b/steam-runtime-tools/xdg-portal.h
new file mode 100644
index 0000000000000000000000000000000000000000..d691ff8adca0b89e20a7fe445ccff207d69d2088
--- /dev/null
+++ b/steam-runtime-tools/xdg-portal.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#pragma once
+
+#if !defined(_SRT_IN_SINGLE_HEADER) && !defined(_SRT_COMPILATION)
+#error "Do not include directly, use <steam-runtime-tools/steam-runtime-tools.h>"
+#endif
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <steam-runtime-tools/macros.h>
+
+typedef struct _SrtXdgPortalInterface SrtXdgPortalInterface;
+typedef struct _SrtXdgPortalInterfaceClass SrtXdgPortalInterfaceClass;
+
+#define SRT_TYPE_XDG_PORTAL_INTERFACE srt_xdg_portal_interface_get_type ()
+#define SRT_XDG_PORTAL_INTERFACE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SRT_TYPE_XDG_PORTAL_INTERFACE, SrtXdgPortalInterface))
+#define SRT_XDG_PORTAL_INTERFACE_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), SRT_TYPE_XDG_PORTAL_INTERFACE, SrtXdgPortalInterfaceClass))
+#define SRT_IS_XDG_PORTAL_INTERFACE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SRT_TYPE_XDG_PORTAL_INTERFACE))
+#define SRT_IS_XDG_PORTAL_INTERFACE_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), SRT_TYPE_XDG_PORTAL_INTERFACE))
+#define SRT_XDG_PORTAL_INTERFACE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SRT_TYPE_XDG_PORTAL_INTERFACE, SrtXdgPortalInterfaceClass)
+
+_SRT_PUBLIC
+GType srt_xdg_portal_interface_get_type (void);
+
+typedef struct _SrtXdgPortalBackend SrtXdgPortalBackend;
+typedef struct _SrtXdgPortalBackendClass SrtXdgPortalBackendClass;
+
+#define SRT_TYPE_XDG_PORTAL_BACKEND srt_xdg_portal_backend_get_type ()
+#define SRT_XDG_PORTAL_BACKEND(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SRT_TYPE_XDG_PORTAL_BACKEND, SrtXdgPortalBackend))
+#define SRT_XDG_PORTAL_BACKEND_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST ((cls), SRT_TYPE_XDG_PORTAL_BACKEND, SrtXdgPortalBackendClass))
+#define SRT_IS_XDG_PORTAL_BACKEND(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SRT_TYPE_XDG_PORTAL_BACKEND))
+#define SRT_IS_XDG_PORTAL_BACKEND_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE ((cls), SRT_TYPE_XDG_PORTAL_BACKEND))
+#define SRT_XDG_PORTAL_BACKEND_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SRT_TYPE_XDG_PORTAL_BACKEND, SrtXdgPortalBackendClass)
+
+_SRT_PUBLIC
+GType srt_xdg_portal_backend_get_type (void);
+
+/**
+ * SrtXdgPortalIssues:
+ * @SRT_XDG_PORTAL_ISSUES_NONE: There are no problems
+ * @SRT_XDG_PORTAL_ISSUES_UNKNOWN: A generic internal error occurred while
+ *  trying to check the XDG portals support, or, while reading a report,
+ *  either an unknown issue flag was encountered or the xdg portal issues
+ *  field was missing
+ * @SRT_XDG_PORTAL_ISSUES_TIMEOUT: The check for the XDG portals support
+ *  took too long to run and was terminated. This is likely to indicate
+ *  that there are issues that caused the process to hang.
+ * @SRT_XDG_PORTAL_ISSUES_MISSING_INTERFACE: A certain required XDG portal
+ *  interface is missing.
+ * @SRT_XDG_PORTAL_ISSUES_NO_IMPLEMENTATION: There isn't a working XDG portal
+ *  implementation.
+ */
+typedef enum
+{
+  SRT_XDG_PORTAL_ISSUES_NONE = 0,
+  SRT_XDG_PORTAL_ISSUES_UNKNOWN = (1 << 0),
+  SRT_XDG_PORTAL_ISSUES_TIMEOUT = (1 << 1),
+  SRT_XDG_PORTAL_ISSUES_MISSING_INTERFACE = (1 << 2),
+  SRT_XDG_PORTAL_ISSUES_NO_IMPLEMENTATION = (1 << 3),
+} SrtXdgPortalIssues;
+
+_SRT_PUBLIC
+const char *srt_xdg_portal_backend_get_name (SrtXdgPortalBackend *self);
+_SRT_PUBLIC
+gboolean srt_xdg_portal_backend_is_available (SrtXdgPortalBackend *self);
+
+_SRT_PUBLIC
+const char *srt_xdg_portal_interface_get_name (SrtXdgPortalInterface *self);
+_SRT_PUBLIC
+gboolean srt_xdg_portal_interface_is_available (SrtXdgPortalInterface *self);
+_SRT_PUBLIC
+guint32 srt_xdg_portal_interface_get_version (SrtXdgPortalInterface *self);
+
+#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtXdgPortalBackend, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtXdgPortalInterface, g_object_unref)
+#endif
diff --git a/tests/json-report/full-good-report.json b/tests/json-report/full-good-report.json
index 9edf332b24bbea8291d145520a645acb4ecfac97..c56e4e85ab08023e8e13e55448af1e0b75b632b6 100644
--- a/tests/json-report/full-good-report.json
+++ b/tests/json-report/full-good-report.json
@@ -247,6 +247,29 @@
       "steam_uri_handler" : true
     }
   ],
+  "xdg-portals" : {
+    "details" : {
+      "interfaces" : {
+        "org.freedesktop.portal.OpenURI" : {
+          "available" : true,
+          "version" : 3
+        },
+        "org.freedesktop.portal.Email" : {
+          "available" : true,
+          "version" : 2
+        }
+      },
+      "backends" : {
+        "org.freedesktop.impl.portal.desktop.gtk" : {
+          "available" : true
+        },
+        "org.freedesktop.impl.portal.desktop.kde" : {
+          "available" : false
+        }
+      }
+    },
+    "issues" : []
+  },
   "cpu-features" : {
     "x86-64" : true,
     "sse3" : false,
diff --git a/tests/json-report/partial-report.json b/tests/json-report/partial-report.json
index a7fec110712b1f7fd6f2dfc9f6329f85b7afa6df..92d2d5e1b3980fb99d31eab6f87263c48dc2ae0c 100644
--- a/tests/json-report/partial-report.json
+++ b/tests/json-report/partial-report.json
@@ -41,5 +41,11 @@
         "error" : "Something went wrong"
       }
     ]
+  },
+  "xdg-portals" : {
+    "issues" : [
+      "timeout"
+    ],
+    "messages" : "timeout: failed to run command ‘x86_64-linux-gnu-check-xdg-portal’: No such file or directory\n"
   }
 }
diff --git a/tests/meson.build b/tests/meson.build
index bc04ff99677b9b92aecec8ffc2be4b3ac5bf2e09..42355686599692629b71c6df0261eb7c738ff1b7 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -35,6 +35,7 @@ tests = [
   {'name': 'locale'},
   {'name': 'system-info'},
   {'name': 'utils', 'static': true},
+  {'name': 'xdg-portal'},
 ]
 
 if get_option('bin')
@@ -261,10 +262,14 @@ foreach helper : [
   'mock-bad-check-va-api',
   'mock-bad-check-vdpau',
   'mock-bad-check-vulkan',
+  'mock-bad-check-xdg-portal',
   'mock-bad-vulkaninfo',
   'mock-good-check-va-api',
   'mock-good-check-vdpau',
+  'mock-good-check-xdg-portal',
+  'mock-good-flatpak-check-xdg-portal',
   'mock-good-vulkaninfo',
+  'mock-hanging-check-xdg-portal',
   'mock-hanging-wflinfo',
   'mock-sigusr-wflinfo',
 ]
diff --git a/tests/mock-bad-check-xdg-portal.c b/tests/mock-bad-check-xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..f0d5f1577b7c56a4a245fddd05fcc9ea6ac4fc9d
--- /dev/null
+++ b/tests/mock-bad-check-xdg-portal.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+int
+main (int argc,
+      char **argv)
+{
+  // Give bad output
+  fprintf (stderr, "The 'version' property is not available for 'org.freedesktop.portal.OpenURI', either there isn't a working xdg-desktop-portal or it is a very old version\n");
+
+  printf ("{\n"
+          "\t\"interfaces\" : {\n"
+          "\t\t\"org.freedesktop.portal.OpenURI\" : {\n"
+          "\t\t\t\"available\" : false\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.portal.Email\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 3\n"
+          "\t\t}\n"
+          "\t},\n"
+          "\t\"backends\" : {\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.gtk\" : {\n"
+          "\t\t\t\"available\" : true\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.kde\" : {\n"
+          "\t\t\t\"available\" : false\n"
+          "\t\t}\n"
+          "\t}\n"
+          "}\n");
+
+  return 1;
+}
+
diff --git a/tests/mock-good-check-xdg-portal.c b/tests/mock-good-check-xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..ce658f33f632da7ff34a87921a35d3fd83a2c2e0
--- /dev/null
+++ b/tests/mock-good-check-xdg-portal.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+int
+main (int argc,
+      char **argv)
+{
+  // Give good output
+  printf ("{\n"
+          "\t\"interfaces\" : {\n"
+          "\t\t\"org.freedesktop.portal.OpenURI\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 2\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.portal.Email\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 3\n"
+          "\t\t}\n"
+          "\t},\n"
+          "\t\"backends\" : {\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.gtk\" : {\n"
+          "\t\t\t\"available\" : true\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.kde\" : {\n"
+          "\t\t\t\"available\" : false\n"
+          "\t\t}\n"
+          "\t}\n"
+          "}\n");
+
+  return 0;
+}
+
diff --git a/tests/mock-good-flatpak-check-xdg-portal.c b/tests/mock-good-flatpak-check-xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..0780c7e1efcc44263a38c5b53143a8d0519420f4
--- /dev/null
+++ b/tests/mock-good-flatpak-check-xdg-portal.c
@@ -0,0 +1,48 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+int
+main (int argc,
+      char **argv)
+{
+  // Give good output as if we were in a Flatpak environment
+  printf ("{\n"
+          "\t\"interfaces\" : {\n"
+          "\t\t\"org.freedesktop.portal.OpenURI\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 3\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.portal.Email\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 3\n"
+          "\t\t}\n"
+          "\t}\n"
+          "}\n");
+
+  return 0;
+}
+
diff --git a/tests/mock-hanging-check-xdg-portal.c b/tests/mock-hanging-check-xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..b02ae303cd56852bbad2b668e81e49588e48ec16
--- /dev/null
+++ b/tests/mock-hanging-check-xdg-portal.c
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+
+int
+main (int argc,
+      char **argv)
+{
+  sleep (300); // Sleep for 5 minutes
+
+  // Give good output
+  printf ("{\n"
+          "\t\"interfaces\" : {\n"
+          "\t\t\"org.freedesktop.portal.OpenURI\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 2\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.portal.Email\" : {\n"
+          "\t\t\t\"available\" : true,\n"
+          "\t\t\t\"version\" : 3\n"
+          "\t\t}\n"
+          "\t},\n"
+          "\t\"backends\" : {\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.gtk\" : {\n"
+          "\t\t\t\"available\" : true\n"
+          "\t\t},\n"
+          "\t\t\"org.freedesktop.impl.portal.desktop.kde\" : {\n"
+          "\t\t\t\"available\" : false\n"
+          "\t\t}\n"
+          "\t}\n"
+          "}\n");
+
+  return 0;
+}
+
diff --git a/tests/system-info.c b/tests/system-info.c
index 7b4e4fffedc82e9aa5a9eee28bc08e137b3b2fc7..30dc5e4f8d953f6529259b208c8acbed59e577fd 100644
--- a/tests/system-info.c
+++ b/tests/system-info.c
@@ -2449,6 +2449,21 @@ typedef struct
   gboolean steam_handler;
 } DesktopEntryTest;
 
+typedef struct
+{
+  const gchar *name;
+  gboolean available;
+  guint32 version;
+} XdgPortalInfoTest;
+
+typedef struct
+{
+  XdgPortalInfoTest interfaces[3];
+  XdgPortalInfoTest backends[3];
+  SrtXdgPortalIssues issues;
+  const gchar *messages;
+} XdgPortalTest;
+
 typedef struct
 {
   const gchar *description;
@@ -2465,6 +2480,7 @@ typedef struct
   IcdTest egl_icd[3];
   IcdTest vulkan_icd[3];
   DesktopEntryTest desktop_entry[3];
+  XdgPortalTest xdg_portal;
   SrtX86FeatureFlags x86_features;
   SrtX86FeatureFlags x86_known;
 } JsonTest;
@@ -2758,6 +2774,34 @@ static const JsonTest json_test[] =
       },
     },
 
+    .xdg_portal =
+    {
+      .interfaces =
+      {
+        {
+          .name = "org.freedesktop.portal.OpenURI",
+          .available = TRUE,
+          .version = 3,
+        },
+        {
+          .name = "org.freedesktop.portal.Email",
+          .available = TRUE,
+          .version = 2,
+        },
+      },
+      .backends =
+      {
+        {
+          .name = "org.freedesktop.impl.portal.desktop.gtk",
+          .available = TRUE,
+        },
+        {
+          .name = "org.freedesktop.impl.portal.desktop.kde",
+          .available = FALSE,
+        },
+      },
+    },
+
     .x86_features = SRT_X86_FEATURE_X86_64 | SRT_X86_FEATURE_CMPXCHG16B,
     .x86_known = SRT_X86_FEATURE_X86_64 | SRT_X86_FEATURE_SSE3 | SRT_X86_FEATURE_CMPXCHG16B,
 
@@ -2829,6 +2873,11 @@ static const JsonTest json_test[] =
         .error_message = "Something went wrong",
       },
     },
+    .xdg_portal =
+    {
+      .issues = SRT_XDG_PORTAL_ISSUES_TIMEOUT,
+      .messages = "timeout: failed to run command ‘x86_64-linux-gnu-check-xdg-portal’: No such file or directory\n",
+    },
     .x86_features = SRT_X86_FEATURE_NONE,
     .x86_known = SRT_X86_FEATURE_NONE,
   }, /* End Partial JSON report */
@@ -2875,6 +2924,10 @@ static const JsonTest json_test[] =
         .error_message = "(missing error message)",
       },
     },
+    .xdg_portal =
+    {
+      .issues = SRT_XDG_PORTAL_ISSUES_UNKNOWN,
+    },
     .x86_features = SRT_X86_FEATURE_NONE,
     .x86_known = SRT_X86_FEATURE_NONE,
   }, /* End Partial-2 JSON report */
@@ -2904,6 +2957,10 @@ static const JsonTest json_test[] =
       },
     },
     .locale_issues = SRT_LOCALE_ISSUES_UNKNOWN,
+    .xdg_portal =
+    {
+      .issues = SRT_XDG_PORTAL_ISSUES_UNKNOWN,
+    },
     .x86_features = SRT_X86_FEATURE_NONE,
     .x86_known = SRT_X86_FEATURE_NONE,
   }, /* End Empty JSON report */
@@ -2946,6 +3003,10 @@ static const JsonTest json_test[] =
       }
     },
     .locale_issues = SRT_LOCALE_ISSUES_C_UTF8_MISSING | SRT_LOCALE_ISSUES_UNKNOWN,
+    .xdg_portal =
+    {
+      .issues = SRT_XDG_PORTAL_ISSUES_UNKNOWN,
+    },
     .x86_features = SRT_X86_FEATURE_X86_64 | SRT_X86_FEATURE_SSE3 | SRT_X86_FEATURE_UNKNOWN,
     .x86_known = SRT_X86_FEATURE_X86_64 | SRT_X86_FEATURE_SSE3 | SRT_X86_FEATURE_CMPXCHG16B | SRT_X86_FEATURE_UNKNOWN,
   }, /* End Newer JSON report */
@@ -2975,8 +3036,12 @@ json_parsing (Fixture *f,
       gchar *name;
       gchar *pretty_name;
       gchar *host_directory;
+      g_autoptr(SrtObjectList) portal_interfaces = NULL;
+      g_autoptr(SrtObjectList) portal_backends = NULL;
+      g_autofree gchar *portal_messages = NULL;
       g_autofree gchar *steamscript_path = NULL;
       g_autofree gchar *steamscript_version = NULL;
+      SrtXdgPortalIssues issues;
       gchar **pinned_32 = NULL;
       gchar **messages_32 = NULL;
       gchar **pinned_64 = NULL;
@@ -3211,6 +3276,33 @@ json_parsing (Fixture *f,
                            srt_desktop_entry_is_steam_handler (iter->data));
         }
 
+      portal_interfaces = srt_system_info_list_xdg_portal_interfaces (info);
+      for (j = 0, iter = portal_interfaces; iter != NULL; iter = iter->next, j++)
+        {
+          g_assert_cmpstr (t->xdg_portal.interfaces[j].name, ==,
+                           srt_xdg_portal_interface_get_name (iter->data));
+          g_assert_cmpint (t->xdg_portal.interfaces[j].available, ==,
+                           srt_xdg_portal_interface_is_available (iter->data));
+          g_assert_cmpint (t->xdg_portal.interfaces[j].version, ==,
+                           srt_xdg_portal_interface_get_version (iter->data));
+        }
+      g_assert_cmpstr (t->xdg_portal.interfaces[j].name, ==, NULL);
+
+      portal_backends = srt_system_info_list_xdg_portal_backends (info);
+      for (j = 0, iter = portal_backends; iter != NULL; iter = iter->next, j++)
+        {
+          g_assert_cmpstr (t->xdg_portal.backends[j].name, ==,
+                           srt_xdg_portal_backend_get_name (iter->data));
+          g_assert_cmpint (t->xdg_portal.backends[j].available, ==,
+                           srt_xdg_portal_backend_is_available (iter->data));
+        }
+      g_assert_cmpstr (t->xdg_portal.backends[j].name, ==, NULL);
+
+      issues = srt_system_info_get_xdg_portal_issues (info, &portal_messages);
+
+      g_assert_cmpint (issues, ==, t->xdg_portal.issues);
+      g_assert_cmpstr (portal_messages, ==, t->xdg_portal.messages);
+
       g_assert_cmpint (t->x86_features, ==, srt_system_info_get_x86_features (info));
       g_assert_cmpint (t->x86_known, ==, srt_system_info_get_known_x86_features (info));
 
diff --git a/tests/xdg-portal.c b/tests/xdg-portal.c
new file mode 100644
index 0000000000000000000000000000000000000000..305105435ed35cdc5177964cab01b702091ab9e1
--- /dev/null
+++ b/tests/xdg-portal.c
@@ -0,0 +1,257 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/* Include this before steam-runtime-tools.h so that its backport of
+ * G_DEFINE_AUTOPTR_CLEANUP_FUNC will be visible to it */
+#include "steam-runtime-tools/glib-backports-internal.h"
+
+#include <steam-runtime-tools/steam-runtime-tools.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <glib-object.h>
+
+#include "steam-runtime-tools/xdg-portal-internal.h"
+#include "test-utils.h"
+
+static const char *argv0;
+
+typedef struct
+{
+  gchar *builddir;
+} Fixture;
+
+typedef struct
+{
+  int unused;
+} Config;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR"));
+
+  if (f->builddir == NULL)
+    f->builddir = g_path_get_dirname (argv0);
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  g_free (f->builddir);
+}
+
+typedef struct
+{
+  const gchar *name;
+  gboolean is_available;
+} XdgPortalBackendTest;
+
+typedef struct
+{
+  const gchar *name;
+  gboolean is_available;
+  guint32 version;
+} XdgPortalInterfaceTest;
+
+typedef struct
+{
+  const gchar *description;
+  const gchar *multiarch_tuple;
+  const gchar *messages;
+  const gchar *sysroot;
+  SrtTestFlags test_flags;
+  SrtXdgPortalIssues issues;
+  XdgPortalInterfaceTest xdg_portal_interfaces_info[3];
+  XdgPortalBackendTest xdg_portal_backends_info[3];
+} XdgPortalTest;
+
+static const XdgPortalTest xdg_portal_test[] =
+{
+  {
+    .description = "Missing OpenURI portal",
+    .multiarch_tuple = "mock-bad",
+    .messages = "The 'version' property is not available for 'org.freedesktop.portal.OpenURI', "
+                "either there isn't a working xdg-desktop-portal or it is a very old version\n",
+    .issues = SRT_XDG_PORTAL_ISSUES_MISSING_INTERFACE,
+    .xdg_portal_interfaces_info =
+    {
+      {
+        .name = "org.freedesktop.portal.OpenURI",
+        .is_available = FALSE,
+        .version = 0,
+      },
+      {
+        .name = "org.freedesktop.portal.Email",
+        .is_available = TRUE,
+        .version = 3,
+      },
+    },
+    .xdg_portal_backends_info =
+    {
+      {
+        .name = "org.freedesktop.impl.portal.desktop.gtk",
+        .is_available = TRUE,
+      },
+      {
+        .name = "org.freedesktop.impl.portal.desktop.kde",
+        .is_available = FALSE,
+      },
+    },
+  },
+
+  {
+    .description = "Good system check",
+    .multiarch_tuple = "mock-good",
+    .issues = SRT_XDG_PORTAL_ISSUES_NONE,
+    .xdg_portal_interfaces_info =
+    {
+      {
+        .name = "org.freedesktop.portal.OpenURI",
+        .is_available = TRUE,
+        .version = 2,
+      },
+      {
+        .name = "org.freedesktop.portal.Email",
+        .is_available = TRUE,
+        .version = 3,
+      },
+    },
+    .xdg_portal_backends_info =
+    {
+      {
+        .name = "org.freedesktop.impl.portal.desktop.gtk",
+        .is_available = TRUE,
+      },
+      {
+        .name = "org.freedesktop.impl.portal.desktop.kde",
+        .is_available = FALSE,
+      },
+    },
+  },
+
+  {
+    .description = "Good Flatpak environment",
+    .multiarch_tuple = "mock-good-flatpak",
+    .sysroot = "flatpak-example",
+    .issues = SRT_XDG_PORTAL_ISSUES_NONE,
+    .xdg_portal_interfaces_info =
+    {
+      {
+        .name = "org.freedesktop.portal.OpenURI",
+        .is_available = TRUE,
+        .version = 3,
+      },
+      {
+        .name = "org.freedesktop.portal.Email",
+        .is_available = TRUE,
+        .version = 3,
+      },
+    },
+  },
+
+  {
+    .description = "XDG portal check timeout",
+    .multiarch_tuple = "mock-hanging",
+    .test_flags = SRT_TEST_FLAGS_TIME_OUT_SOONER,
+    .issues = SRT_XDG_PORTAL_ISSUES_TIMEOUT,
+  },
+};
+
+static void
+test_check_xdg_portal (Fixture *f,
+                       gconstpointer context)
+{
+  for (gsize i = 0; i < G_N_ELEMENTS (xdg_portal_test); i++)
+    {
+      const XdgPortalTest *t = &xdg_portal_test[i];
+      g_autoptr(SrtSystemInfo) info = NULL;
+      g_autoptr(SrtObjectList) portal_interfaces = NULL;
+      GList *portal_backends = NULL;
+      SrtXdgPortalIssues issues;
+      g_autofree gchar *messages = NULL;
+      g_autofree gchar *sysroot = NULL;
+      GList *iter;
+      gsize j;
+
+      g_test_message ("%s", t->description);
+
+      info = srt_system_info_new (NULL);
+      srt_system_info_set_helpers_path (info, f->builddir);
+      srt_system_info_set_primary_multiarch_tuple (info, t->multiarch_tuple);
+      srt_system_info_set_test_flags (info, t->test_flags);
+      if (t->sysroot != NULL)
+        {
+          sysroot = g_build_filename (f->builddir, "sysroots", t->sysroot, NULL);
+          srt_system_info_set_sysroot (info, sysroot);
+        }
+
+      portal_interfaces = srt_system_info_list_xdg_portal_interfaces (info);
+      for (j = 0, iter = portal_interfaces; iter != NULL; iter = iter->next, j++)
+        {
+          g_assert_cmpstr (t->xdg_portal_interfaces_info[j].name, ==,
+                           srt_xdg_portal_interface_get_name (iter->data));
+          g_assert_cmpint (t->xdg_portal_interfaces_info[j].is_available, ==,
+                           srt_xdg_portal_interface_is_available (iter->data));
+          g_assert_cmpint (t->xdg_portal_interfaces_info[j].version, ==,
+                           srt_xdg_portal_interface_get_version (iter->data));
+        }
+      g_assert_cmpstr (t->xdg_portal_interfaces_info[j].name, ==, NULL);
+
+      portal_backends = srt_system_info_list_xdg_portal_backends (info);
+      for (j = 0, iter = portal_backends; iter != NULL; iter = iter->next, j++)
+        {
+          g_assert_cmpstr (t->xdg_portal_backends_info[j].name, ==,
+                           srt_xdg_portal_backend_get_name (iter->data));
+          g_assert_cmpint (t->xdg_portal_backends_info[j].is_available, ==,
+                           srt_xdg_portal_backend_is_available (iter->data));
+        }
+      g_assert_cmpstr (t->xdg_portal_backends_info[j].name, ==, NULL);
+
+      issues = srt_system_info_get_xdg_portal_issues (info, &messages);
+
+      g_assert_cmpint (issues, ==, t->issues);
+      g_assert_cmpstr (messages, ==, t->messages);
+    }
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  argv0 = argv[0];
+
+  g_test_init (&argc, &argv, NULL);
+  g_test_add ("/xdg-portal/test_check_xdg_portal", Fixture, NULL, setup,
+              test_check_xdg_portal, teardown);
+
+  return g_test_run ();
+}