From 2fa9c43b74b33c5cb53d94e2b57f1243ce7c9a4d Mon Sep 17 00:00:00 2001
From: Ryan Gonzalez <rymg19@gmail.com>
Date: Thu, 1 Jul 2021 18:58:00 +0000
Subject: [PATCH] Implement support for the XDG file chooser portal

This allows use on Linux of the XDG portal for file selection rather
than directly calling out to the GTK/KDE dialogs. When the feature is
enabled, a test is run asynchronously with the main thread's UI
initialization to determine if the file chooser portal is available on
the bus, and if so, it will be used as the default once the first dialog
is opened.

In order for this to work, support is added to the GtkUi platform code
to "export" a window handle that can be shared with other clients of the
display server (a hex ID on X11 or an xdg-foreign handle on Wayland).
This handle can then be passed to the portal to ensure a transient
dialog is created.

Bug: 885292
Change-Id: I98ac13e93af59bc5069b68fac5c0767b93502391
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2992214
Reviewed-by: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Nick Yamane <nickdiego@igalia.com>
Reviewed-by: Ryo Hashimoto <hashimoto@chromium.org>
Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#897839}
---
 AUTHORS                                       |   1 +
 ui/base/linux/linux_ui_delegate.cc            |   2 +-
 ui/base/linux/linux_ui_delegate.h             |   2 +-
 ui/gtk/BUILD.gn                               |   5 +
 ui/gtk/DEPS                                   |   2 +
 ui/gtk/gtk_ui.cc                              |   4 +
 ui/gtk/gtk_ui_platform.h                      |   9 +
 ui/gtk/native_theme_gtk_unittest.cc           |   2 +
 ui/gtk/select_file_dialog_impl.cc             |  83 +-
 ui/gtk/select_file_dialog_impl.h              |   7 +
 ui/gtk/select_file_dialog_impl_portal.cc      | 796 ++++++++++++++++++
 ui/gtk/select_file_dialog_impl_portal.h       | 202 +++++
 ui/gtk/wayland/gtk_ui_platform_wayland.cc     |  29 +-
 ui/gtk/wayland/gtk_ui_platform_wayland.h      |   7 +-
 ui/gtk/x/gtk_ui_platform_x11.cc               |   8 +
 ui/gtk/x/gtk_ui_platform_x11.h                |   3 +
 .../wayland/host/linux_ui_delegate_wayland.cc |   2 +-
 .../wayland/host/linux_ui_delegate_wayland.h  |   2 +-
 18 files changed, 1127 insertions(+), 39 deletions(-)
 create mode 100644 ui/gtk/select_file_dialog_impl_portal.cc
 create mode 100644 ui/gtk/select_file_dialog_impl_portal.h

diff --git a/AUTHORS b/AUTHORS
index a7487835534c1..4ee5bc174fbf6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -930,6 +930,7 @@ Ruiyi Luo <luoruiyi2008@gmail.com>
 Rulong Chen <rulong.crl@alibaba-inc.com>
 Russell Davis <russell.davis@gmail.com>
 Ryan Ackley <ryanackley@gmail.com>
+Ryan Gonzalez <rymg19@gmail.com>
 Ryan Norton <rnorton10@gmail.com>
 Ryan Sleevi <ryan-chromium-dev@sleevi.com>
 Ryan Yoakum <ryoakum@skobalt.com>
diff --git a/ui/base/linux/linux_ui_delegate.cc b/ui/base/linux/linux_ui_delegate.cc
index 7b0aefd431142..795da1cb03cee 100644
--- a/ui/base/linux/linux_ui_delegate.cc
+++ b/ui/base/linux/linux_ui_delegate.cc
@@ -27,7 +27,7 @@ LinuxUiDelegate::~LinuxUiDelegate() {
   instance_ = nullptr;
 }
 
-bool LinuxUiDelegate::SetWidgetTransientFor(
+bool LinuxUiDelegate::ExportWindowHandle(
     uint32_t parent_widget,
     base::OnceCallback<void(const std::string&)> callback) {
   // This function should not be called when using a platform that doesn't
diff --git a/ui/base/linux/linux_ui_delegate.h b/ui/base/linux/linux_ui_delegate.h
index 1ed7f90c36641..c0a9df8022c5d 100644
--- a/ui/base/linux/linux_ui_delegate.h
+++ b/ui/base/linux/linux_ui_delegate.h
@@ -28,7 +28,7 @@ class COMPONENT_EXPORT(UI_BASE) LinuxUiDelegate {
   virtual LinuxUiBackend GetBackend() const = 0;
 
   // Only implemented on Wayland.
-  virtual bool SetWidgetTransientFor(
+  virtual bool ExportWindowHandle(
       uint32_t parent_widget,
       base::OnceCallback<void(const std::string&)> callback);
 
diff --git a/ui/gtk/BUILD.gn b/ui/gtk/BUILD.gn
index 7bdb5a0e439ab..bef73bd7a6dfc 100644
--- a/ui/gtk/BUILD.gn
+++ b/ui/gtk/BUILD.gn
@@ -69,6 +69,8 @@ component("gtk") {
     "select_file_dialog_impl_gtk.cc",
     "select_file_dialog_impl_gtk.h",
     "select_file_dialog_impl_kde.cc",
+    "select_file_dialog_impl_portal.cc",
+    "select_file_dialog_impl_portal.h",
     "settings_provider.h",
     "settings_provider_gtk.cc",
     "settings_provider_gtk.h",
@@ -90,6 +92,8 @@ component("gtk") {
   deps = [
     ":gtk_stubs",
     "//base",
+    "//components/dbus/thread_linux",
+    "//dbus",
     "//skia",
 
     # GTK pulls pangoft2, which requires HarfBuzz symbols. When linking
@@ -115,6 +119,7 @@ component("gtk") {
     "//ui/shell_dialogs",
     "//ui/strings",
     "//ui/views",
+    "//url",
   ]
 
   if (enable_basic_printing) {
diff --git a/ui/gtk/DEPS b/ui/gtk/DEPS
index 51f523c4958b0..e93e6c1eef302 100644
--- a/ui/gtk/DEPS
+++ b/ui/gtk/DEPS
@@ -1,5 +1,7 @@
 include_rules = [
   "+chrome/browser/themes/theme_properties.h",
+  "+components/dbus/thread_linux",
+  "+dbus",
   "+printing",
   "+third_party/skia",
   "+ui/aura",
diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc
index a3302b63a5c6c..6266d03b50fd7 100644
--- a/ui/gtk/gtk_ui.cc
+++ b/ui/gtk/gtk_ui.cc
@@ -330,6 +330,8 @@ GtkUi::GtkUi() {
   auto backend = delegate ? delegate->GetBackend() : ui::LinuxUiBackend::kX11;
   platform_ = CreateGtkUiPlatform(backend);
 
+  SelectFileDialogImpl::Initialize();
+
   // Avoid GTK initializing atk-bridge, and let AuraLinux implementation
   // do it once it is ready.
   std::unique_ptr<base::Environment> env(base::Environment::Create());
@@ -346,6 +348,8 @@ GtkUi::GtkUi() {
 GtkUi::~GtkUi() {
   DCHECK_EQ(g_gtk_ui, this);
   g_gtk_ui = nullptr;
+
+  SelectFileDialogImpl::Shutdown();
 }
 
 // static
diff --git a/ui/gtk/gtk_ui_platform.h b/ui/gtk/gtk_ui_platform.h
index 8650770cd8c5b..bfd4a98f1f6a1 100644
--- a/ui/gtk/gtk_ui_platform.h
+++ b/ui/gtk/gtk_ui_platform.h
@@ -5,8 +5,11 @@
 #ifndef UI_GTK_GTK_UI_PLATFORM_H_
 #define UI_GTK_GTK_UI_PLATFORM_H_
 
+#include "base/callback_forward.h"
 #include "ui/gfx/native_widget_types.h"
 
+#include <string>
+
 using GdkKeymap = struct _GdkKeymap;
 using GtkWindow = struct _GtkWindow;
 using GtkWidget = struct _GtkWidget;
@@ -33,6 +36,12 @@ class GtkUiPlatform {
   // and is supported only in X11 backend (both Aura and Ozone).
   virtual GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) = 0;
 
+  // Exports a prefixed, platform-dependent (X11 or Wayland) window handle for
+  // an Aura window id, then calls the given callback with the handle.
+  virtual bool ExportWindowHandle(
+      gfx::AcceleratedWidget window_id,
+      base::OnceCallback<void(std::string)> callback) = 0;
+
   // Gtk dialog windows must be set transient for the browser window. This
   // function abstracts away such functionality.
   virtual bool SetGtkWidgetTransientFor(GtkWidget* widget,
diff --git a/ui/gtk/native_theme_gtk_unittest.cc b/ui/gtk/native_theme_gtk_unittest.cc
index a415404b07e9d..a422c2e294965 100644
--- a/ui/gtk/native_theme_gtk_unittest.cc
+++ b/ui/gtk/native_theme_gtk_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/command_line.h"
 #include "base/memory/ptr_util.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/gtk/gtk_ui_factory.h"
@@ -57,6 +58,7 @@ class NativeThemeGtkRedirectedEquivalenceTest
     }
   }
 
+  base::test::TaskEnvironment task_environment_;
   std::unique_ptr<views::LinuxUI> gtk_ui_;
 };
 
diff --git a/ui/gtk/select_file_dialog_impl.cc b/ui/gtk/select_file_dialog_impl.cc
index 36d53b986b8d3..884edef57ae5a 100644
--- a/ui/gtk/select_file_dialog_impl.cc
+++ b/ui/gtk/select_file_dialog_impl.cc
@@ -10,13 +10,16 @@
 #include "base/files/file_util.h"
 #include "base/nix/xdg_util.h"
 #include "base/no_destructor.h"
+#include "base/notreached.h"
 #include "base/threading/thread_restrictions.h"
+#include "ui/gtk/select_file_dialog_impl_portal.h"
 
 namespace {
 
-enum UseKdeFileDialogStatus { UNKNOWN, NO_KDE, YES_KDE };
+enum FileDialogChoice { kUnknown, kGtk, kKde, kPortal };
+
+FileDialogChoice dialog_choice_ = kUnknown;
 
-UseKdeFileDialogStatus use_kde_ = UNKNOWN;
 std::string& KDialogVersion() {
   static base::NoDestructor<std::string> version;
   return *version;
@@ -29,42 +32,68 @@ namespace gtk {
 base::FilePath* SelectFileDialogImpl::last_saved_path_ = nullptr;
 base::FilePath* SelectFileDialogImpl::last_opened_path_ = nullptr;
 
+// static
+void SelectFileDialogImpl::Initialize() {
+  SelectFileDialogImplPortal::StartAvailabilityTestInBackground();
+}
+
+// static
+void SelectFileDialogImpl::Shutdown() {
+  SelectFileDialogImplPortal::DestroyPortalConnection();
+}
+
 // static
 ui::SelectFileDialog* SelectFileDialogImpl::Create(
     ui::SelectFileDialog::Listener* listener,
     std::unique_ptr<ui::SelectFilePolicy> policy) {
-  if (use_kde_ == UNKNOWN) {
-    // Start out assumimg we are not going to use KDE.
-    use_kde_ = NO_KDE;
+  if (dialog_choice_ == kUnknown) {
+    // Start out assumimg we are going to use GTK.
+    dialog_choice_ = kGtk;
 
-    // Check to see if KDE is the desktop environment.
-    std::unique_ptr<base::Environment> env(base::Environment::Create());
-    base::nix::DesktopEnvironment desktop =
-        base::nix::GetDesktopEnvironment(env.get());
-    if (desktop == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
-        desktop == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
-        desktop == base::nix::DESKTOP_ENVIRONMENT_KDE5) {
-      // Check to see if the user dislikes the KDE file dialog.
-      if (!env->HasVar("NO_CHROME_KDE_FILE_DIALOG")) {
-        // Check to see if the KDE dialog works.
-        if (SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread(
-                KDialogVersion())) {
-          use_kde_ = YES_KDE;
+    // Check to see if the portal is available.
+    if (SelectFileDialogImplPortal::IsPortalAvailable()) {
+      dialog_choice_ = kPortal;
+    } else {
+      // Make sure to kill the portal connection.
+      SelectFileDialogImplPortal::DestroyPortalConnection();
+
+      // Check to see if KDE is the desktop environment.
+      std::unique_ptr<base::Environment> env(base::Environment::Create());
+      base::nix::DesktopEnvironment desktop =
+          base::nix::GetDesktopEnvironment(env.get());
+      if (desktop == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
+          desktop == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
+          desktop == base::nix::DESKTOP_ENVIRONMENT_KDE5) {
+        // Check to see if the user dislikes the KDE file dialog.
+        if (!env->HasVar("NO_CHROME_KDE_FILE_DIALOG")) {
+          // Check to see if the KDE dialog works.
+          if (SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread(
+                  KDialogVersion())) {
+            dialog_choice_ = kKde;
+          }
         }
       }
     }
   }
 
-  if (use_kde_ == NO_KDE) {
-    return SelectFileDialogImpl::NewSelectFileDialogImplGTK(listener,
-                                                            std::move(policy));
+  switch (dialog_choice_) {
+    case kGtk:
+      return SelectFileDialogImpl::NewSelectFileDialogImplGTK(
+          listener, std::move(policy));
+    case kPortal:
+      return SelectFileDialogImpl::NewSelectFileDialogImplPortal(
+          listener, std::move(policy));
+    case kKde: {
+      std::unique_ptr<base::Environment> env(base::Environment::Create());
+      base::nix::DesktopEnvironment desktop =
+          base::nix::GetDesktopEnvironment(env.get());
+      return SelectFileDialogImpl::NewSelectFileDialogImplKDE(
+          listener, std::move(policy), desktop, KDialogVersion());
+    }
+    case kUnknown:
+      NOTREACHED();
+      return nullptr;
   }
-
-  std::unique_ptr<base::Environment> env(base::Environment::Create());
-  base::nix::DesktopEnvironment desktop =
-      base::nix::GetDesktopEnvironment(env.get());
-  return SelectFileDialogImpl::NewSelectFileDialogImplKDE(
-      listener, std::move(policy), desktop, KDialogVersion());
 }
 
 SelectFileDialogImpl::SelectFileDialogImpl(
diff --git a/ui/gtk/select_file_dialog_impl.h b/ui/gtk/select_file_dialog_impl.h
index e4698a945b783..987919abbe375 100644
--- a/ui/gtk/select_file_dialog_impl.h
+++ b/ui/gtk/select_file_dialog_impl.h
@@ -24,6 +24,9 @@ namespace gtk {
 // Shared implementation SelectFileDialog used by SelectFileDialogImplGTK
 class SelectFileDialogImpl : public ui::SelectFileDialog {
  public:
+  static void Initialize();
+  static void Shutdown();
+
   // Main factory method which returns correct type.
   static ui::SelectFileDialog* Create(
       Listener* listener,
@@ -39,6 +42,10 @@ class SelectFileDialogImpl : public ui::SelectFileDialog {
       std::unique_ptr<ui::SelectFilePolicy> policy,
       base::nix::DesktopEnvironment desktop,
       const std::string& kdialog_version);
+  // Factory method for creating an XDG portal-backed SelectFileDialogImpl
+  static SelectFileDialogImpl* NewSelectFileDialogImplPortal(
+      Listener* listener,
+      std::unique_ptr<ui::SelectFilePolicy> policy);
 
   // Returns true if the SelectFileDialog class returned by
   // NewSelectFileDialogImplKDE will actually work.
diff --git a/ui/gtk/select_file_dialog_impl_portal.cc b/ui/gtk/select_file_dialog_impl_portal.cc
new file mode 100644
index 0000000000000..89cae70162f37
--- /dev/null
+++ b/ui/gtk/select_file_dialog_impl_portal.cc
@@ -0,0 +1,796 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/gtk/select_file_dialog_impl_portal.h"
+
+#include "base/bind.h"
+#include "base/containers/contains.h"
+#include "base/logging.h"
+#include "base/no_destructor.h"
+#include "base/notreached.h"
+#include "base/strings/string_piece.h"
+#include "base/strings/stringprintf.h"
+#include "components/dbus/thread_linux/dbus_thread_linux.h"
+#include "dbus/object_path.h"
+#include "dbus/property.h"
+#include "ui/aura/window_tree_host.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/linux/linux_ui_delegate.h"
+#include "ui/gfx/native_widget_types.h"
+#include "ui/gtk/gtk_ui.h"
+#include "ui/strings/grit/ui_strings.h"
+#include "url/url_util.h"
+
+namespace gtk {
+
+namespace {
+
+constexpr char kDBusMethodNameHasOwner[] = "NameHasOwner";
+constexpr char kDBusMethodListActivatableNames[] = "ListActivatableNames";
+
+constexpr char kXdgPortalService[] = "org.freedesktop.portal.Desktop";
+constexpr char kXdgPortalObject[] = "/org/freedesktop/portal/desktop";
+
+constexpr int kXdgPortalRequiredVersion = 3;
+
+constexpr char kXdgPortalRequestInterfaceName[] =
+    "org.freedesktop.portal.Request";
+constexpr char kXdgPortalResponseSignal[] = "Response";
+
+constexpr char kFileChooserInterfaceName[] =
+    "org.freedesktop.portal.FileChooser";
+
+constexpr char kFileChooserMethodOpenFile[] = "OpenFile";
+constexpr char kFileChooserMethodSaveFile[] = "SaveFile";
+
+constexpr char kFileChooserOptionHandleToken[] = "handle_token";
+constexpr char kFileChooserOptionAcceptLabel[] = "accept_label";
+constexpr char kFileChooserOptionMultiple[] = "multiple";
+constexpr char kFileChooserOptionDirectory[] = "directory";
+constexpr char kFileChooserOptionFilters[] = "filters";
+constexpr char kFileChooserOptionCurrentFilter[] = "current_filter";
+constexpr char kFileChooserOptionCurrentFolder[] = "current_folder";
+constexpr char kFileChooserOptionCurrentName[] = "current_name";
+
+constexpr int kFileChooserFilterKindGlob = 0;
+
+constexpr char kFileUriPrefix[] = "file://";
+
+struct FileChooserProperties : dbus::PropertySet {
+  dbus::Property<uint32_t> version;
+
+  explicit FileChooserProperties(dbus::ObjectProxy* object_proxy)
+      : dbus::PropertySet(object_proxy, kFileChooserInterfaceName, {}) {
+    RegisterProperty("version", &version);
+  }
+
+  ~FileChooserProperties() override = default;
+};
+
+void AppendStringOption(dbus::MessageWriter* writer,
+                        const std::string& name,
+                        const std::string& value) {
+  dbus::MessageWriter option_writer(nullptr);
+  writer->OpenDictEntry(&option_writer);
+
+  option_writer.AppendString(name);
+  option_writer.AppendVariantOfString(value);
+
+  writer->CloseContainer(&option_writer);
+}
+
+void AppendByteStringOption(dbus::MessageWriter* writer,
+                            const std::string& name,
+                            const std::string& value) {
+  dbus::MessageWriter option_writer(nullptr);
+  writer->OpenDictEntry(&option_writer);
+
+  option_writer.AppendString(name);
+
+  dbus::MessageWriter value_writer(nullptr);
+  option_writer.OpenVariant("ay", &value_writer);
+
+  value_writer.AppendArrayOfBytes(
+      reinterpret_cast<const std::uint8_t*>(value.c_str()),
+      // size + 1 will include the null terminator.
+      value.size() + 1);
+
+  option_writer.CloseContainer(&value_writer);
+  writer->CloseContainer(&option_writer);
+}
+
+void AppendBoolOption(dbus::MessageWriter* writer,
+                      const std::string& name,
+                      bool value) {
+  dbus::MessageWriter option_writer(nullptr);
+  writer->OpenDictEntry(&option_writer);
+
+  option_writer.AppendString(name);
+  option_writer.AppendVariantOfBool(value);
+
+  writer->CloseContainer(&option_writer);
+}
+
+}  // namespace
+
+// static
+SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplPortal(
+    Listener* listener,
+    std::unique_ptr<ui::SelectFilePolicy> policy) {
+  return new SelectFileDialogImplPortal(listener, std::move(policy));
+}
+
+SelectFileDialogImplPortal::SelectFileDialogImplPortal(
+    Listener* listener,
+    std::unique_ptr<ui::SelectFilePolicy> policy)
+    : SelectFileDialogImpl(listener, std::move(policy)) {}
+
+SelectFileDialogImplPortal::~SelectFileDialogImplPortal() = default;
+
+// static
+void SelectFileDialogImplPortal::StartAvailabilityTestInBackground() {
+  if (GetAvailabilityTestCompletionFlag()->IsSet())
+    return;
+
+  dbus_thread_linux::GetTaskRunner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          &SelectFileDialogImplPortal::CheckPortalAvailabilityOnBusThread));
+}
+
+// static
+bool SelectFileDialogImplPortal::IsPortalAvailable() {
+  if (!GetAvailabilityTestCompletionFlag()->IsSet())
+    LOG(WARNING) << "Portal availiability checked before test was complete";
+
+  return is_portal_available_;
+}
+
+// static
+void SelectFileDialogImplPortal::DestroyPortalConnection() {
+  dbus_thread_linux::GetTaskRunner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&SelectFileDialogImplPortal::DestroyBusOnBusThread));
+}
+
+bool SelectFileDialogImplPortal::IsRunning(
+    gfx::NativeWindow parent_window) const {
+  if (parent_window && parent_window->GetHost()) {
+    auto window = parent_window->GetHost()->GetAcceleratedWidget();
+    return parents_.find(window) != parents_.end();
+  }
+
+  return false;
+}
+
+void SelectFileDialogImplPortal::SelectFileImpl(
+    Type type,
+    const std::u16string& title,
+    const base::FilePath& default_path,
+    const FileTypeInfo* file_types,
+    int file_type_index,
+    const base::FilePath::StringType& default_extension,
+    gfx::NativeWindow owning_window,
+    void* params) {
+  auto info = base::MakeRefCounted<DialogInfo>();
+  info->type = type;
+  info->main_task_runner = base::SequencedTaskRunnerHandle::Get();
+  info->listener_params = params;
+
+  if (owning_window && owning_window->GetHost()) {
+    info->parent = owning_window->GetHost()->GetAcceleratedWidget();
+    parents_.insert(*info->parent);
+  }
+
+  if (file_types)
+    file_types_ = *file_types;
+
+  file_type_index_ = file_type_index;
+
+  PortalFilterSet filter_set = BuildFilterSet();
+
+  auto parent_handle_callback = base::BindOnce(
+      &SelectFileDialogImplPortal::SelectFileImplWithParentHandle,
+      base::Unretained(this),
+      // We don't move info, because it will be needed below to cancel the open
+      // on error.
+      info, std::move(title), std::move(default_path), std::move(filter_set),
+      std::move(default_extension));
+
+  if (info->parent) {
+    if (!GtkUi::GetPlatform()->ExportWindowHandle(
+            *info->parent, std::move(parent_handle_callback))) {
+      LOG(ERROR) << "Failed to export window handle for portal select dialog";
+      CancelOpenOnMainThread(std::move(info));
+    }
+  } else {
+    // No parent, so just use a blank parent handle.
+    std::move(parent_handle_callback).Run("");
+  }
+}
+
+bool SelectFileDialogImplPortal::HasMultipleFileTypeChoicesImpl() {
+  return file_types_.extensions.size() > 1;
+}
+
+// static
+void SelectFileDialogImplPortal::CheckPortalAvailabilityOnBusThread() {
+  base::AtomicFlag* availability_test_complete =
+      GetAvailabilityTestCompletionFlag();
+  if (availability_test_complete->IsSet())
+    return;
+
+  dbus::Bus* bus = AcquireBusOnBusThread();
+
+  dbus::ObjectProxy* dbus_proxy =
+      bus->GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
+
+  if (IsPortalRunningOnBusThread(dbus_proxy) ||
+      IsPortalActivatableOnBusThread(dbus_proxy)) {
+    dbus::ObjectPath portal_path(kXdgPortalObject);
+    dbus::ObjectProxy* portal =
+        bus->GetObjectProxy(kXdgPortalService, portal_path);
+
+    FileChooserProperties properties(portal);
+    if (!properties.GetAndBlock(&properties.version)) {
+      LOG(ERROR) << "Failed to read portal version property";
+    } else if (properties.version.value() >= kXdgPortalRequiredVersion) {
+      is_portal_available_ = true;
+    }
+  }
+
+  VLOG(1) << "File chooser portal available: "
+          << (is_portal_available_ ? "yes" : "no");
+  availability_test_complete->Set();
+}
+
+// static
+bool SelectFileDialogImplPortal::IsPortalRunningOnBusThread(
+    dbus::ObjectProxy* dbus_proxy) {
+  dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, kDBusMethodNameHasOwner);
+  dbus::MessageWriter writer(&method_call);
+  writer.AppendString(kXdgPortalService);
+
+  std::unique_ptr<dbus::Response> response = dbus_proxy->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!response)
+    return false;
+
+  dbus::MessageReader reader(response.get());
+  bool owned = false;
+  if (!reader.PopBool(&owned)) {
+    LOG(ERROR) << "Failed to read response";
+    return false;
+  }
+
+  return owned;
+}
+
+// static
+bool SelectFileDialogImplPortal::IsPortalActivatableOnBusThread(
+    dbus::ObjectProxy* dbus_proxy) {
+  dbus::MethodCall method_call(DBUS_INTERFACE_DBUS,
+                               kDBusMethodListActivatableNames);
+
+  std::unique_ptr<dbus::Response> response = dbus_proxy->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!response)
+    return false;
+
+  dbus::MessageReader reader(response.get());
+  std::vector<std::string> names;
+  if (!reader.PopArrayOfStrings(&names)) {
+    LOG(ERROR) << "Failed to read response";
+    return false;
+  }
+
+  return base::Contains(names, kXdgPortalService);
+}
+
+SelectFileDialogImplPortal::PortalFilter::PortalFilter() = default;
+SelectFileDialogImplPortal::PortalFilter::PortalFilter(
+    const PortalFilter& other) = default;
+SelectFileDialogImplPortal::PortalFilter::PortalFilter(PortalFilter&& other) =
+    default;
+SelectFileDialogImplPortal::PortalFilter::~PortalFilter() = default;
+
+SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet() = default;
+SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet(
+    const PortalFilterSet& other) = default;
+SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet(
+    PortalFilterSet&& other) = default;
+SelectFileDialogImplPortal::PortalFilterSet::~PortalFilterSet() = default;
+
+SelectFileDialogImplPortal::DialogInfo::DialogInfo() = default;
+SelectFileDialogImplPortal::DialogInfo::~DialogInfo() = default;
+
+// static
+scoped_refptr<dbus::Bus>*
+SelectFileDialogImplPortal::AcquireBusStorageOnBusThread() {
+  static base::NoDestructor<scoped_refptr<dbus::Bus>> bus(nullptr);
+  if (!*bus) {
+    dbus::Bus::Options options;
+    options.bus_type = dbus::Bus::SESSION;
+    options.connection_type = dbus::Bus::PRIVATE;
+    options.dbus_task_runner = dbus_thread_linux::GetTaskRunner();
+
+    *bus = base::MakeRefCounted<dbus::Bus>(options);
+  }
+
+  return bus.get();
+}
+
+// static
+dbus::Bus* SelectFileDialogImplPortal::AcquireBusOnBusThread() {
+  return AcquireBusStorageOnBusThread()->get();
+}
+
+void SelectFileDialogImplPortal::DestroyBusOnBusThread() {
+  scoped_refptr<dbus::Bus>* bus_storage = AcquireBusStorageOnBusThread();
+  (*bus_storage)->ShutdownAndBlock();
+
+  // If the connection is restarted later on, we need to make sure the entire
+  // bus is newly created. Otherwise, references to an old, invalid task runner
+  // may persist.
+  bus_storage->reset();
+}
+
+// static
+base::AtomicFlag*
+SelectFileDialogImplPortal::GetAvailabilityTestCompletionFlag() {
+  static base::NoDestructor<base::AtomicFlag> flag;
+  return flag.get();
+}
+
+SelectFileDialogImplPortal::PortalFilterSet
+SelectFileDialogImplPortal::BuildFilterSet() {
+  PortalFilterSet filter_set;
+
+  for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
+    PortalFilter filter;
+
+    for (const std::string& extension : file_types_.extensions[i]) {
+      if (extension.empty())
+        continue;
+
+      filter.patterns.insert(base::StringPrintf("*.%s", extension.c_str()));
+    }
+
+    if (filter.patterns.empty())
+      continue;
+
+    // If there is no matching description, use a default description based on
+    // the filter.
+    if (i < file_types_.extension_description_overrides.size()) {
+      filter.name =
+          base::UTF16ToUTF8(file_types_.extension_description_overrides[i]);
+    } else {
+      std::vector<std::string> patterns_vector(filter.patterns.begin(),
+                                               filter.patterns.end());
+      filter.name = base::JoinString(patterns_vector, ",");
+    }
+
+    if (i == file_type_index_)
+      filter_set.default_filter = filter;
+
+    filter_set.filters.push_back(std::move(filter));
+  }
+
+  if (file_types_.include_all_files && !filter_set.filters.empty()) {
+    // Add the *.* filter, but only if we have added other filters (otherwise it
+    // is implied).
+    PortalFilter filter;
+    filter.name = l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES);
+    filter.patterns.insert("*.*");
+
+    filter_set.filters.push_back(std::move(filter));
+  }
+
+  return filter_set;
+}
+
+void SelectFileDialogImplPortal::SelectFileImplWithParentHandle(
+    scoped_refptr<DialogInfo> info,
+    std::u16string title,
+    base::FilePath default_path,
+    PortalFilterSet filter_set,
+    base::FilePath::StringType default_extension,
+    std::string parent_handle) {
+  dbus_thread_linux::GetTaskRunner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&SelectFileDialogImplPortal::SelectFileImplOnBusThread,
+                     base::Unretained(this), std::move(info), std::move(title),
+                     std::move(default_path), std::move(filter_set),
+                     std::move(default_extension), std::move(parent_handle)));
+}
+
+void SelectFileDialogImplPortal::SelectFileImplOnBusThread(
+    scoped_refptr<DialogInfo> info,
+    std::u16string title,
+    base::FilePath default_path,
+    PortalFilterSet filter_set,
+    base::FilePath::StringType default_extension,
+    std::string parent_handle) {
+  dbus::Bus* bus = AcquireBusOnBusThread();
+  if (!bus->Connect())
+    LOG(ERROR) << "Could not connect to bus for XDG portal";
+
+  std::string method;
+  switch (info->type) {
+    case SELECT_FOLDER:
+    case SELECT_UPLOAD_FOLDER:
+    case SELECT_EXISTING_FOLDER:
+    case SELECT_OPEN_FILE:
+    case SELECT_OPEN_MULTI_FILE:
+      method = kFileChooserMethodOpenFile;
+      break;
+    case SELECT_SAVEAS_FILE:
+      method = kFileChooserMethodSaveFile;
+      break;
+    case SELECT_NONE:
+      NOTREACHED();
+      break;
+  }
+
+  dbus::MethodCall method_call(kFileChooserInterfaceName, method);
+  dbus::MessageWriter writer(&method_call);
+
+  writer.AppendString(parent_handle);
+
+  if (!title.empty()) {
+    writer.AppendString(base::UTF16ToUTF8(title));
+  } else {
+    int message_id = 0;
+    if (info->type == SELECT_SAVEAS_FILE) {
+      message_id = IDS_SAVEAS_ALL_FILES;
+    } else if (info->type == SELECT_OPEN_MULTI_FILE) {
+      message_id = IDS_OPEN_FILES_DIALOG_TITLE;
+    } else {
+      message_id = IDS_OPEN_FILE_DIALOG_TITLE;
+    }
+    writer.AppendString(l10n_util::GetStringUTF8(message_id));
+  }
+
+  std::string response_handle_token =
+      base::StringPrintf("handle_%d", handle_token_counter_++);
+
+  AppendOptions(&writer, info->type, response_handle_token, default_path,
+                filter_set);
+
+  // The sender part of the handle object contains the D-Bus connection name
+  // without the prefix colon and with all dots replaced with underscores.
+  std::string sender_part;
+  base::ReplaceChars(bus->GetConnectionName().substr(1), ".", "_",
+                     &sender_part);
+
+  dbus::ObjectPath expected_handle_path(
+      base::StringPrintf("/org/freedesktop/portal/desktop/request/%s/%s",
+                         sender_part.c_str(), response_handle_token.c_str()));
+
+  info->response_handle =
+      bus->GetObjectProxy(kXdgPortalService, expected_handle_path);
+  ConnectToHandle(info);
+
+  dbus::ObjectPath portal_path(kXdgPortalObject);
+  dbus::ObjectProxy* portal =
+      bus->GetObjectProxy(kXdgPortalService, portal_path);
+  portal->CallMethodWithErrorResponse(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
+      base::BindOnce(&SelectFileDialogImplPortal::OnCallResponse,
+                     base::Unretained(this), base::Unretained(bus), info));
+}
+
+void SelectFileDialogImplPortal::AppendOptions(
+    dbus::MessageWriter* writer,
+    Type type,
+    const std::string& response_handle_token,
+    const base::FilePath& default_path,
+    const PortalFilterSet& filter_set) {
+  dbus::MessageWriter options_writer(nullptr);
+  writer->OpenArray("{sv}", &options_writer);
+
+  AppendStringOption(&options_writer, kFileChooserOptionHandleToken,
+                     response_handle_token);
+
+  if (type == SELECT_UPLOAD_FOLDER) {
+    AppendStringOption(&options_writer, kFileChooserOptionAcceptLabel,
+                       l10n_util::GetStringUTF8(
+                           IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON));
+  }
+
+  if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER ||
+      type == SELECT_EXISTING_FOLDER) {
+    AppendBoolOption(&options_writer, kFileChooserOptionDirectory, true);
+  } else if (type == SELECT_OPEN_MULTI_FILE) {
+    AppendBoolOption(&options_writer, kFileChooserOptionMultiple, true);
+  }
+
+  if (type == SELECT_SAVEAS_FILE && !default_path.empty()) {
+    if (CallDirectoryExistsOnUIThread(default_path)) {
+      // If this is an existing directory, navigate to that directory, with no
+      // filename.
+      AppendByteStringOption(&options_writer, kFileChooserOptionCurrentFolder,
+                             default_path.value());
+    } else {
+      // The default path does not exist, or is an existing file. We use
+      // current_folder followed by current_name, as per the recommendation of
+      // the GTK docs and the pattern followed by SelectFileDialogImplGTK.
+      AppendByteStringOption(&options_writer, kFileChooserOptionCurrentFolder,
+                             default_path.DirName().value());
+      AppendStringOption(&options_writer, kFileChooserOptionCurrentName,
+                         default_path.BaseName().value());
+    }
+  }
+
+  AppendFiltersOption(&options_writer, filter_set.filters);
+  if (filter_set.default_filter) {
+    dbus::MessageWriter option_writer(nullptr);
+    options_writer.OpenDictEntry(&option_writer);
+
+    option_writer.AppendString(kFileChooserOptionCurrentFilter);
+
+    dbus::MessageWriter value_writer(nullptr);
+    option_writer.OpenVariant("(sa(us))", &value_writer);
+
+    AppendFilterStruct(&value_writer, *filter_set.default_filter);
+
+    option_writer.CloseContainer(&value_writer);
+    options_writer.CloseContainer(&option_writer);
+  }
+
+  writer->CloseContainer(&options_writer);
+}
+
+void SelectFileDialogImplPortal::AppendFiltersOption(
+    dbus::MessageWriter* writer,
+    const std::vector<PortalFilter>& filters) {
+  dbus::MessageWriter option_writer(nullptr);
+  writer->OpenDictEntry(&option_writer);
+
+  option_writer.AppendString(kFileChooserOptionFilters);
+
+  dbus::MessageWriter variant_writer(nullptr);
+  option_writer.OpenVariant("a(sa(us))", &variant_writer);
+
+  dbus::MessageWriter filters_writer(nullptr);
+  variant_writer.OpenArray("(sa(us))", &filters_writer);
+
+  for (const PortalFilter& filter : filters) {
+    AppendFilterStruct(&filters_writer, filter);
+  }
+
+  variant_writer.CloseContainer(&filters_writer);
+  option_writer.CloseContainer(&variant_writer);
+  writer->CloseContainer(&option_writer);
+}
+
+void SelectFileDialogImplPortal::AppendFilterStruct(
+    dbus::MessageWriter* writer,
+    const PortalFilter& filter) {
+  dbus::MessageWriter filter_writer(nullptr);
+  writer->OpenStruct(&filter_writer);
+
+  filter_writer.AppendString(filter.name);
+
+  dbus::MessageWriter patterns_writer(nullptr);
+  filter_writer.OpenArray("(us)", &patterns_writer);
+
+  for (const std::string& pattern : filter.patterns) {
+    dbus::MessageWriter pattern_writer(nullptr);
+    patterns_writer.OpenStruct(&pattern_writer);
+
+    pattern_writer.AppendUint32(kFileChooserFilterKindGlob);
+    pattern_writer.AppendString(pattern);
+
+    patterns_writer.CloseContainer(&pattern_writer);
+  }
+
+  filter_writer.CloseContainer(&patterns_writer);
+  writer->CloseContainer(&filter_writer);
+}
+
+void SelectFileDialogImplPortal::ConnectToHandle(
+    scoped_refptr<DialogInfo> info) {
+  info->response_handle->ConnectToSignal(
+      kXdgPortalRequestInterfaceName, kXdgPortalResponseSignal,
+      base::BindRepeating(&SelectFileDialogImplPortal::OnResponseSignalEmitted,
+                          base::Unretained(this), info),
+      base::BindOnce(&SelectFileDialogImplPortal::OnResponseSignalConnected,
+                     base::Unretained(this), info));
+}
+
+void SelectFileDialogImplPortal::CompleteOpen(
+    scoped_refptr<DialogInfo> info,
+    std::vector<base::FilePath> paths) {
+  info->response_handle->Detach();
+  info->main_task_runner->PostTask(
+      FROM_HERE,
+      base::BindOnce(&SelectFileDialogImplPortal::CompleteOpenOnMainThread,
+                     base::Unretained(this), std::move(info),
+                     std::move(paths)));
+}
+
+void SelectFileDialogImplPortal::CancelOpen(scoped_refptr<DialogInfo> info) {
+  info->response_handle->Detach();
+  info->main_task_runner->PostTask(
+      FROM_HERE,
+      base::BindOnce(&SelectFileDialogImplPortal::CancelOpenOnMainThread,
+                     base::Unretained(this), std::move(info)));
+}
+
+void SelectFileDialogImplPortal::CompleteOpenOnMainThread(
+    scoped_refptr<DialogInfo> info,
+    std::vector<base::FilePath> paths) {
+  UnparentOnMainThread(info.get());
+
+  if (listener_) {
+    if (info->type == SELECT_OPEN_MULTI_FILE) {
+      listener_->MultiFilesSelected(paths, info->listener_params);
+    } else if (paths.size() > 1) {
+      LOG(ERROR) << "Got >1 file URI from a single-file chooser";
+    } else {
+      // The meaning of the index isn't clear, and we can't determine what
+      // filter was selected regardless, see select_file_dialog_impl_kde.cc.
+      listener_->FileSelected(paths.front(), 1, info->listener_params);
+    }
+  }
+}
+
+void SelectFileDialogImplPortal::CancelOpenOnMainThread(
+    scoped_refptr<DialogInfo> info) {
+  UnparentOnMainThread(info.get());
+
+  if (listener_)
+    listener_->FileSelectionCanceled(info->listener_params);
+}
+
+void SelectFileDialogImplPortal::UnparentOnMainThread(DialogInfo* info) {
+  if (info->parent) {
+    parents_.erase(*info->parent);
+    info->parent.reset();
+  }
+}
+
+void SelectFileDialogImplPortal::OnCallResponse(
+    dbus::Bus* bus,
+    scoped_refptr<DialogInfo> info,
+    dbus::Response* response,
+    dbus::ErrorResponse* error_response) {
+  if (response) {
+    dbus::MessageReader reader(response);
+    dbus::ObjectPath actual_handle_path;
+    if (!reader.PopObjectPath(&actual_handle_path)) {
+      LOG(ERROR) << "Invalid portal response";
+    } else {
+      if (info->response_handle->object_path() != actual_handle_path) {
+        VLOG(1) << "Re-attaching response handle to "
+                << actual_handle_path.value();
+
+        info->response_handle->Detach();
+        info->response_handle =
+            bus->GetObjectProxy(kXdgPortalService, actual_handle_path);
+        ConnectToHandle(info);
+      }
+
+      // Return before the operation is cancelled.
+      return;
+    }
+  } else if (error_response) {
+    std::string error_name = error_response->GetErrorName();
+    std::string error_message;
+    dbus::MessageReader reader(error_response);
+    reader.PopString(&error_message);
+
+    LOG(ERROR) << "Portal returned error: " << error_name << ": "
+               << error_message;
+  } else {
+    NOTREACHED();
+  }
+
+  // All error paths end up here.
+  CancelOpen(std::move(info));
+}
+
+void SelectFileDialogImplPortal::OnResponseSignalConnected(
+    scoped_refptr<DialogInfo> info,
+    const std::string& interface,
+    const std::string& signal,
+    bool connected) {
+  if (!connected) {
+    LOG(ERROR) << "Could not connect to Response signal";
+    CancelOpen(std::move(info));
+  }
+}
+
+void SelectFileDialogImplPortal::OnResponseSignalEmitted(
+    scoped_refptr<DialogInfo> info,
+    dbus::Signal* signal) {
+  dbus::MessageReader reader(signal);
+
+  std::vector<std::string> uris;
+  if (!CheckResponseCode(&reader) || !ReadResponseResults(&reader, &uris)) {
+    CancelOpen(std::move(info));
+    return;
+  }
+
+  std::vector<base::FilePath> paths = ConvertUrisToPaths(uris);
+  if (!paths.empty())
+    CompleteOpen(std::move(info), std::move(paths));
+  else
+    CancelOpen(std::move(info));
+}
+
+bool SelectFileDialogImplPortal::CheckResponseCode(
+    dbus::MessageReader* reader) {
+  std::uint32_t response = 0;
+  if (!reader->PopUint32(&response)) {
+    LOG(ERROR) << "Failed to read response ID";
+    return false;
+  } else if (response != 0) {
+    return false;
+  }
+
+  return true;
+}
+
+bool SelectFileDialogImplPortal::ReadResponseResults(
+    dbus::MessageReader* reader,
+    std::vector<std::string>* uris) {
+  dbus::MessageReader results_reader(nullptr);
+  if (!reader->PopArray(&results_reader)) {
+    LOG(ERROR) << "Failed to read file chooser variant";
+    return false;
+  }
+
+  while (results_reader.HasMoreData()) {
+    dbus::MessageReader entry_reader(nullptr);
+    std::string key;
+    if (!results_reader.PopDictEntry(&entry_reader) ||
+        !entry_reader.PopString(&key)) {
+      LOG(ERROR) << "Failed to read response entry";
+      return false;
+    }
+
+    if (key == "uris") {
+      dbus::MessageReader uris_reader(nullptr);
+      if (!entry_reader.PopVariant(&uris_reader) ||
+          !uris_reader.PopArrayOfStrings(uris)) {
+        LOG(ERROR) << "Failed to read response entry value";
+        return false;
+      }
+
+      break;
+    }
+  }
+
+  return true;
+}
+
+std::vector<base::FilePath> SelectFileDialogImplPortal::ConvertUrisToPaths(
+    const std::vector<std::string>& uris) {
+  std::vector<base::FilePath> paths;
+  for (const std::string& uri : uris) {
+    if (!base::StartsWith(uri, kFileUriPrefix, base::CompareCase::SENSITIVE)) {
+      LOG(WARNING) << "Ignoring unknown file chooser URI: " << uri;
+      continue;
+    }
+
+    base::StringPiece encoded_path(uri);
+    encoded_path.remove_prefix(strlen(kFileUriPrefix));
+
+    url::RawCanonOutputT<char16_t> decoded_path;
+    url::DecodeURLEscapeSequences(encoded_path.data(), encoded_path.size(),
+                                  url::DecodeURLMode::kUTF8OrIsomorphic,
+                                  &decoded_path);
+    paths.emplace_back(base::UTF16ToUTF8(
+        base::StringPiece16(decoded_path.data(), decoded_path.length())));
+  }
+
+  return paths;
+}
+
+bool SelectFileDialogImplPortal::is_portal_available_ = false;
+int SelectFileDialogImplPortal::handle_token_counter_ = 0;
+
+}  // namespace gtk
diff --git a/ui/gtk/select_file_dialog_impl_portal.h b/ui/gtk/select_file_dialog_impl_portal.h
new file mode 100644
index 0000000000000..62fce7255b2e5
--- /dev/null
+++ b/ui/gtk/select_file_dialog_impl_portal.h
@@ -0,0 +1,202 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
+#define UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
+
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/sequenced_task_runner.h"
+#include "base/synchronization/atomic_flag.h"
+#include "dbus/bus.h"
+#include "dbus/message.h"
+#include "dbus/object_proxy.h"
+#include "ui/gtk/select_file_dialog_impl.h"
+
+namespace gtk {
+
+// Implementation of SelectFileDialog that has the XDG file chooser portal show
+// a platform-dependent file selection dialog. This acts as a modal dialog.
+class SelectFileDialogImplPortal : public SelectFileDialogImpl {
+ public:
+  SelectFileDialogImplPortal(Listener* listener,
+                             std::unique_ptr<ui::SelectFilePolicy> policy);
+
+  SelectFileDialogImplPortal(const SelectFileDialogImplPortal& other) = delete;
+  SelectFileDialogImplPortal& operator=(
+      const SelectFileDialogImplPortal& other) = delete;
+
+  // Starts running a test to check for the presence of the file chooser portal
+  // on the D-Bus task runner. This should only be called once, preferably
+  // around program start.
+  static void StartAvailabilityTestInBackground();
+
+  // Checks if the file chooser portal is available. Blocks if the availability
+  // test from above has not yet completed (which should generally not happen).
+  static bool IsPortalAvailable();
+
+  // Destroys the connection to the bus.
+  static void DestroyPortalConnection();
+
+ protected:
+  ~SelectFileDialogImplPortal() override;
+
+  // BaseShellDialog implementation:
+  bool IsRunning(gfx::NativeWindow parent_window) const override;
+
+  // SelectFileDialog implementation.
+  // |params| is user data we pass back via the Listener interface.
+  void SelectFileImpl(Type type,
+                      const std::u16string& title,
+                      const base::FilePath& default_path,
+                      const FileTypeInfo* file_types,
+                      int file_type_index,
+                      const base::FilePath::StringType& default_extension,
+                      gfx::NativeWindow owning_window,
+                      void* params) override;
+
+  bool HasMultipleFileTypeChoicesImpl() override;
+
+ private:
+  // A named set of patterns used as a dialog filter.
+  struct PortalFilter {
+    PortalFilter();
+    PortalFilter(const PortalFilter& other);
+    PortalFilter(PortalFilter&& other);
+    ~PortalFilter();
+
+    PortalFilter& operator=(const PortalFilter& other) = default;
+    PortalFilter& operator=(PortalFilter&& other) = default;
+
+    std::string name;
+    std::set<std::string> patterns;
+  };
+
+  // A set of PortalFilters, potentially with a default.
+  struct PortalFilterSet {
+    PortalFilterSet();
+    PortalFilterSet(const PortalFilterSet& other);
+    PortalFilterSet(PortalFilterSet&& other);
+    ~PortalFilterSet();
+
+    PortalFilterSet& operator=(const PortalFilterSet& other) = default;
+    PortalFilterSet& operator=(PortalFilterSet&& other) = default;
+
+    std::vector<PortalFilter> filters;
+    absl::optional<PortalFilter> default_filter;
+  };
+
+  // A wrapper over some shared contextual information that needs to be passed
+  // around between various handler functions. This is ref-counted due to some
+  // of the locations its used in having slightly unclear or error-prone
+  // lifetimes.
+  struct DialogInfo : base::RefCountedThreadSafe<DialogInfo> {
+    DialogInfo();
+
+    // The response object handle that the portal will send a signal to upon the
+    // dialog's completion.
+    dbus::ObjectProxy* response_handle = nullptr;
+    absl::optional<gfx::AcceleratedWidget> parent;
+    Type type;
+    // The task runner the SelectFileImpl method was called on.
+    scoped_refptr<base::SequencedTaskRunner> main_task_runner;
+    // The untyped params to pass to the listener.
+    void* listener_params = nullptr;
+
+   private:
+    friend class base::RefCountedThreadSafe<DialogInfo>;
+
+    ~DialogInfo();
+  };
+
+  static scoped_refptr<dbus::Bus>* AcquireBusStorageOnBusThread();
+  static dbus::Bus* AcquireBusOnBusThread();
+
+  static void DestroyBusOnBusThread();
+
+  static void CheckPortalAvailabilityOnBusThread();
+
+  static bool IsPortalRunningOnBusThread(dbus::ObjectProxy* dbus_proxy);
+  static bool IsPortalActivatableOnBusThread(dbus::ObjectProxy* dbus_proxy);
+
+  // Returns a flag, written by the D-Bus thread and read by the UI thread,
+  // indicating whether or not the availability test has completed.
+  static base::AtomicFlag* GetAvailabilityTestCompletionFlag();
+
+  PortalFilterSet BuildFilterSet();
+
+  void SelectFileImplWithParentHandle(
+      scoped_refptr<DialogInfo> info,
+      std::u16string title,
+      base::FilePath default_path,
+      PortalFilterSet filter_set,
+      base::FilePath::StringType default_extension,
+      std::string parent_handle);
+
+  void SelectFileImplOnBusThread(scoped_refptr<DialogInfo> info,
+                                 std::u16string title,
+                                 base::FilePath default_path,
+                                 PortalFilterSet filter_set,
+                                 base::FilePath::StringType default_extension,
+                                 std::string parent_handle);
+
+  void AppendOptions(dbus::MessageWriter* writer,
+                     Type type,
+                     const std::string& response_handle_token,
+                     const base::FilePath& default_path,
+                     const PortalFilterSet& filter_set);
+  void AppendFiltersOption(dbus::MessageWriter* writer,
+                           const std::vector<PortalFilter>& filters);
+  void AppendFilterStruct(dbus::MessageWriter* writer,
+                          const PortalFilter& filter);
+
+  // Sets up listeners for the response handle's signals.
+  void ConnectToHandle(scoped_refptr<DialogInfo> info);
+
+  // Completes an open call, notifying the listener with the given paths, and
+  // marks the dialog as closed.
+  void CompleteOpen(scoped_refptr<DialogInfo> info,
+                    std::vector<base::FilePath> paths);
+  // Completes an open call, notifying the listener with a cancellation, and
+  // marks the dialog as closed.
+  void CancelOpen(scoped_refptr<DialogInfo> info);
+
+  void CompleteOpenOnMainThread(scoped_refptr<DialogInfo> info,
+                                std::vector<base::FilePath> paths);
+  void CancelOpenOnMainThread(scoped_refptr<DialogInfo> info);
+
+  // Removes the DialogInfo parent. Must be called on the UI task runner.
+  void UnparentOnMainThread(DialogInfo* info);
+
+  void OnCallResponse(dbus::Bus* bus,
+                      scoped_refptr<DialogInfo> info,
+                      dbus::Response* response,
+                      dbus::ErrorResponse* error_response);
+
+  void OnResponseSignalConnected(scoped_refptr<DialogInfo> info,
+                                 const std::string& interface,
+                                 const std::string& signal,
+                                 bool connected);
+
+  void OnResponseSignalEmitted(scoped_refptr<DialogInfo> info,
+                               dbus::Signal* signal);
+
+  bool CheckResponseCode(dbus::MessageReader* reader);
+  bool ReadResponseResults(dbus::MessageReader* reader,
+                           std::vector<std::string>* uris);
+  std::vector<base::FilePath> ConvertUrisToPaths(
+      const std::vector<std::string>& uris);
+
+  std::set<gfx::AcceleratedWidget> parents_;
+
+  // Written by the D-Bus thread and read by the UI thread.
+  static bool is_portal_available_;
+
+  // Used by the D-Bus thread to generate unique handle tokens.
+  static int handle_token_counter_;
+};
+
+}  // namespace gtk
+
+#endif  // UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
diff --git a/ui/gtk/wayland/gtk_ui_platform_wayland.cc b/ui/gtk/wayland/gtk_ui_platform_wayland.cc
index d7d503d824583..05e5d59cc54ed 100644
--- a/ui/gtk/wayland/gtk_ui_platform_wayland.cc
+++ b/ui/gtk/wayland/gtk_ui_platform_wayland.cc
@@ -38,9 +38,9 @@ GdkWindow* GtkUiPlatformWayland::GetGdkWindow(
   return nullptr;
 }
 
-bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
-    GtkWidget* widget,
-    gfx::AcceleratedWidget parent) {
+bool GtkUiPlatformWayland::ExportWindowHandle(
+    gfx::AcceleratedWidget window_id,
+    base::OnceCallback<void(std::string)> callback) {
   if (!gtk::GtkCheckVersion(3, 22)) {
     LOG(WARNING) << "set_transient_for_exported not supported in GTK version "
                  << gtk_get_major_version() << '.' << gtk_get_minor_version()
@@ -48,8 +48,17 @@ bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
     return false;
   }
 
-  return ui::LinuxUiDelegate::GetInstance()->SetWidgetTransientFor(
-      parent, base::BindOnce(&GtkUiPlatformWayland::OnHandle,
+  return ui::LinuxUiDelegate::GetInstance()->ExportWindowHandle(
+      window_id,
+      base::BindOnce(&GtkUiPlatformWayland::OnHandleForward,
+                     weak_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
+    GtkWidget* widget,
+    gfx::AcceleratedWidget parent) {
+  return ui::LinuxUiDelegate::GetInstance()->ExportWindowHandle(
+      parent, base::BindOnce(&GtkUiPlatformWayland::OnHandleSetTransient,
                              weak_factory_.GetWeakPtr(), widget));
 }
 
@@ -63,8 +72,8 @@ void GtkUiPlatformWayland::ShowGtkWindow(GtkWindow* window) {
   gtk_window_present(window);
 }
 
-void GtkUiPlatformWayland::OnHandle(GtkWidget* widget,
-                                    const std::string& handle) {
+void GtkUiPlatformWayland::OnHandleSetTransient(GtkWidget* widget,
+                                                const std::string& handle) {
   char* parent = const_cast<char*>(handle.c_str());
   if (gtk::GtkCheckVersion(4)) {
     auto* toplevel = GlibCast<GdkToplevel>(
@@ -77,6 +86,12 @@ void GtkUiPlatformWayland::OnHandle(GtkWidget* widget,
   }
 }
 
+void GtkUiPlatformWayland::OnHandleForward(
+    base::OnceCallback<void(std::string)> callback,
+    const std::string& handle) {
+  std::move(callback).Run("wayland:" + handle);
+}
+
 int GtkUiPlatformWayland::GetGdkKeyState() {
   return ui::LinuxUiDelegate::GetInstance()->GetKeyState();
 }
diff --git a/ui/gtk/wayland/gtk_ui_platform_wayland.h b/ui/gtk/wayland/gtk_ui_platform_wayland.h
index 41eae190208d6..4156367a1cafc 100644
--- a/ui/gtk/wayland/gtk_ui_platform_wayland.h
+++ b/ui/gtk/wayland/gtk_ui_platform_wayland.h
@@ -24,6 +24,9 @@ class GtkUiPlatformWayland : public GtkUiPlatform {
   void OnInitialized(GtkWidget* widget) override;
   GdkKeymap* GetGdkKeymap() override;
   GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) override;
+  bool ExportWindowHandle(
+      gfx::AcceleratedWidget window_id,
+      base::OnceCallback<void(std::string)> callback) override;
   bool SetGtkWidgetTransientFor(GtkWidget* widget,
                                 gfx::AcceleratedWidget parent) override;
   void ClearTransientFor(gfx::AcceleratedWidget parent) override;
@@ -33,7 +36,9 @@ class GtkUiPlatformWayland : public GtkUiPlatform {
  private:
   // Called when xdg-foreign exports a parent window passed in
   // SetGtkWidgetTransientFor.
-  void OnHandle(GtkWidget* widget, const std::string& handle);
+  void OnHandleSetTransient(GtkWidget* widget, const std::string& handle);
+  void OnHandleForward(base::OnceCallback<void(std::string)> callback,
+                       const std::string& handle);
 
   base::WeakPtrFactory<GtkUiPlatformWayland> weak_factory_{this};
 };
diff --git a/ui/gtk/x/gtk_ui_platform_x11.cc b/ui/gtk/x/gtk_ui_platform_x11.cc
index a61f472e7a878..6c47556fbcbba 100644
--- a/ui/gtk/x/gtk_ui_platform_x11.cc
+++ b/ui/gtk/x/gtk_ui_platform_x11.cc
@@ -6,6 +6,7 @@
 
 #include "base/check.h"
 #include "base/environment.h"
+#include "base/strings/stringprintf.h"
 #include "ui/base/x/x11_util.h"
 #include "ui/events/platform/x11/x11_event_source.h"
 #include "ui/gfx/native_widget_types.h"
@@ -59,6 +60,13 @@ GdkWindow* GtkUiPlatformX11::GetGdkWindow(gfx::AcceleratedWidget window_id) {
   return gdk_window;
 }
 
+bool GtkUiPlatformX11::ExportWindowHandle(
+    gfx::AcceleratedWidget window_id,
+    base::OnceCallback<void(std::string)> callback) {
+  std::move(callback).Run(base::StringPrintf("x11:%#x", window_id));
+  return true;
+}
+
 bool GtkUiPlatformX11::SetGtkWidgetTransientFor(GtkWidget* widget,
                                                 gfx::AcceleratedWidget parent) {
   auto x11_window = static_cast<x11::Window>(
diff --git a/ui/gtk/x/gtk_ui_platform_x11.h b/ui/gtk/x/gtk_ui_platform_x11.h
index e96d888f7661c..a899fd3ef4654 100644
--- a/ui/gtk/x/gtk_ui_platform_x11.h
+++ b/ui/gtk/x/gtk_ui_platform_x11.h
@@ -27,6 +27,9 @@ class GtkUiPlatformX11 : public GtkUiPlatform {
   void OnInitialized(GtkWidget* widget) override;
   GdkKeymap* GetGdkKeymap() override;
   GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) override;
+  bool ExportWindowHandle(
+      gfx::AcceleratedWidget window_id,
+      base::OnceCallback<void(std::string)> callback) override;
   bool SetGtkWidgetTransientFor(GtkWidget* widget,
                                 gfx::AcceleratedWidget parent) override;
   void ClearTransientFor(gfx::AcceleratedWidget parent) override;
diff --git a/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.cc b/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.cc
index 34e539bcede7f..480de9625ca31 100644
--- a/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.cc
+++ b/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.cc
@@ -29,7 +29,7 @@ LinuxUiBackend LinuxUiDelegateWayland::GetBackend() const {
   return LinuxUiBackend::kWayland;
 }
 
-bool LinuxUiDelegateWayland::SetWidgetTransientFor(
+bool LinuxUiDelegateWayland::ExportWindowHandle(
     gfx::AcceleratedWidget parent,
     base::OnceCallback<void(const std::string&)> callback) {
   auto* parent_window =
diff --git a/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.h b/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.h
index 3c9d9c4bb3d99..8195ffc137a78 100644
--- a/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.h
+++ b/ui/ozone/platform/wayland/host/linux_ui_delegate_wayland.h
@@ -19,7 +19,7 @@ class LinuxUiDelegateWayland : public LinuxUiDelegate {
 
   // LinuxUiDelegate:
   LinuxUiBackend GetBackend() const override;
-  bool SetWidgetTransientFor(
+  bool ExportWindowHandle(
       gfx::AcceleratedWidget parent,
       base::OnceCallback<void(const std::string&)> callback) override;
   int GetKeyState() override;