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;