Repository: electron/electron
Pull request: fix: xdg portal version detection for file dialogs on linux

#### Description of Change

Fixes https://github.com/electron/electron/issues/46652

Context https://github.com/electron/electron/issues/46652#issuecomment-2846088555

#### Release Notes

Notes: fix xdg portal version detection for file dialogs on linux

Changed files:
filename: filenames.gni
patch: @@ -35,6 +35,7 @@ filenames = {
     "shell/browser/relauncher_linux.cc",
     "shell/browser/ui/electron_desktop_window_tree_host_linux.cc",
     "shell/browser/ui/file_dialog_linux.cc",
+    "shell/browser/ui/file_dialog_linux_portal.cc",
     "shell/browser/ui/gtk/menu_gtk.cc",
     "shell/browser/ui/gtk/menu_gtk.h",
     "shell/browser/ui/gtk/menu_util.cc",

filename: patches/chromium/feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch
patch: @@ -10,6 +10,8 @@ This CL adds support for the following features to //shell_dialogs:
 
 It also:
 * Changes XDG Portal implementation behavior to set default path regardless of dialog type.
+* XDG Portal implementation calls into //electron to perform version checks on the dbus thread
+  Refs https://github.com/electron/electron/issues/46652.
 
 This may be partially upstreamed to Chromium in the future.
 
@@ -345,83 +347,52 @@ index 9d45ec49a4fb5e12407b65b83c1ba0c13cd0dfd8..400cce91b020ecd5e48566f125515d2c
 +
  }  // namespace ui
 diff --git a/ui/shell_dialogs/select_file_dialog_linux_portal.cc b/ui/shell_dialogs/select_file_dialog_linux_portal.cc
-index b23d357d4e21f10be82f0ea37b1ec3e959fc2c0b..d29de35ac813cc68b1faa11e803ace9a78df74ba 100644
+index b23d357d4e21f10be82f0ea37b1ec3e959fc2c0b..e768afc05b42d4d026c88f1516d4e9db84e8dff6 100644
 --- a/ui/shell_dialogs/select_file_dialog_linux_portal.cc
 +++ b/ui/shell_dialogs/select_file_dialog_linux_portal.cc
-@@ -12,6 +12,7 @@
- #include <string_view>
- 
- #include "base/check.h"
-+#include "base/command_line.h"
- #include "base/files/file_util.h"
- #include "base/functional/bind.h"
- #include "base/logging.h"
-@@ -45,6 +46,8 @@ namespace {
- constexpr char kXdgPortalService[] = "org.freedesktop.portal.Desktop";
- constexpr char kXdgPortalObject[] = "/org/freedesktop/portal/desktop";
- 
-+// Version 4 includes support for current_folder option to the OpenFile method via
-+// https://github.com/flatpak/xdg-desktop-portal/commit/71165a5.
- constexpr int kXdgPortalRequiredVersion = 3;
- 
- constexpr char kFileChooserInterfaceName[] =
-@@ -66,6 +69,8 @@ constexpr uint32_t kFileChooserFilterKindGlob = 0;
- 
- constexpr char kFileUriPrefix[] = "file://";
- 
-+const char kXdgPortalRequiredVersionFlag[] = "xdg-portal-required-version";
-+
- enum class ServiceAvailability {
-   kNotStarted,
-   kInProgress,
-@@ -75,6 +80,9 @@ enum class ServiceAvailability {
- 
- ServiceAvailability g_service_availability = ServiceAvailability::kNotStarted;
- 
-+uint32_t g_available_portal_version = 0;
-+uint32_t g_required_portal_version = kXdgPortalRequiredVersion;
-+
- scoped_refptr<base::SequencedTaskRunner>& GetMainTaskRunner() {
-   static base::NoDestructor<scoped_refptr<base::SequencedTaskRunner>>
-       main_task_runner;
-@@ -94,9 +102,10 @@ void OnGetPropertyReply(dbus::Response* response) {
-     return;
-   }
- 
--  g_service_availability = version >= kXdgPortalRequiredVersion
-+  g_service_availability = version >= g_required_portal_version
-                                ? ServiceAvailability::kAvailable
+@@ -28,6 +28,7 @@
+ #include "dbus/message.h"
+ #include "dbus/object_path.h"
+ #include "dbus/object_proxy.h"
++#include "electron/shell/browser/ui/file_dialog.h"
+ #include "ui/aura/window_tree_host.h"
+ #include "ui/base/l10n/l10n_util.h"
+ #include "ui/gfx/native_widget_types.h"
+@@ -99,7 +100,7 @@ void OnGetPropertyReply(dbus::Response* response) {
                                 : ServiceAvailability::kNotAvailable;
-+  g_available_portal_version = version;
  }
  
- void OnServiceStarted(std::optional<bool> service_started) {
-@@ -164,6 +173,12 @@ void SelectFileDialogLinuxPortal::StartAvailabilityTestInBackground() {
-   }
-   g_service_availability = ServiceAvailability::kInProgress;
+-void OnServiceStarted(std::optional<bool> service_started) {
++[[maybe_unused]] void OnServiceStarted(std::optional<bool> service_started) {
+   if (!service_started.value_or(false)) {
+     g_service_availability = ServiceAvailability::kNotAvailable;
+     return;
+@@ -166,18 +167,24 @@ void SelectFileDialogLinuxPortal::StartAvailabilityTestInBackground() {
  
-+  auto* cmd = base::CommandLine::ForCurrentProcess();
-+  if (!base::StringToUint(cmd->GetSwitchValueASCII(kXdgPortalRequiredVersionFlag),
-+                          &g_required_portal_version)) {
-+    g_required_portal_version = kXdgPortalRequiredVersion;
-+  }
-+
    GetMainTaskRunner() = base::SequencedTaskRunner::GetCurrentDefault();
  
++#if 0
    dbus_utils::CheckForServiceAndStart(dbus_thread_linux::GetSharedSessionBus(),
-@@ -180,6 +195,11 @@ bool SelectFileDialogLinuxPortal::IsPortalAvailable() {
+                                       kXdgPortalService,
+                                       base::BindOnce(&OnServiceStarted));
++#endif
++  file_dialog::StartPortalAvailabilityTestInBackground();
+ }
+ 
+ // static
+ bool SelectFileDialogLinuxPortal::IsPortalAvailable() {
++#if 0
+   if (g_service_availability == ServiceAvailability::kInProgress) {
+     LOG(WARNING) << "Portal availability checked before test was complete";
+   }
+ 
    return g_service_availability == ServiceAvailability::kAvailable;
++#endif
++  return file_dialog::IsPortalAvailable();
  }
  
-+// static
-+uint32_t SelectFileDialogLinuxPortal::GetPortalVersion() {
-+  return g_available_portal_version;
-+}
-+
  bool SelectFileDialogLinuxPortal::IsRunning(
-     gfx::NativeWindow parent_window) const {
-   return parent_window && host_ && host_.get() == parent_window->GetHost();
-@@ -382,11 +402,14 @@ DbusDictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
+@@ -382,11 +389,14 @@ DbusDictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
      const PortalFilterSet& filter_set) {
    DbusDictionary dict;
  
@@ -439,7 +410,7 @@ index b23d357d4e21f10be82f0ea37b1ec3e959fc2c0b..d29de35ac813cc68b1faa11e803ace9a
        [[fallthrough]];
      case SelectFileDialog::SELECT_FOLDER:
      case SelectFileDialog::Type::SELECT_EXISTING_FOLDER:
-@@ -399,6 +422,10 @@ DbusDictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
+@@ -399,6 +409,10 @@ DbusDictionary SelectFileDialogLinuxPortal::BuildOptionsDictionary(
        break;
    }
  
@@ -450,17 +421,3 @@ index b23d357d4e21f10be82f0ea37b1ec3e959fc2c0b..d29de35ac813cc68b1faa11e803ace9a
    if (!default_path.empty()) {
      if (default_path_exists) {
        // If this is an existing directory, navigate to that directory, with no
-diff --git a/ui/shell_dialogs/select_file_dialog_linux_portal.h b/ui/shell_dialogs/select_file_dialog_linux_portal.h
-index 651684b1840eaff664f3d73d99bbea40e097c866..9a9d541f1e9586d9d545f8547d3f09ff33dce48d 100644
---- a/ui/shell_dialogs/select_file_dialog_linux_portal.h
-+++ b/ui/shell_dialogs/select_file_dialog_linux_portal.h
-@@ -45,6 +45,9 @@ class SelectFileDialogLinuxPortal : public SelectFileDialogLinux {
-   // availability test has not yet completed.
-   static bool IsPortalAvailable();
- 
-+  // Get version of portal if available.
-+  static uint32_t GetPortalVersion();
-+
-  protected:
-   ~SelectFileDialogLinuxPortal() override;
- 

filename: shell/browser/ui/file_dialog.h
patch: @@ -77,6 +77,16 @@ bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path);
 void ShowSaveDialog(const DialogSettings& settings,
                     gin_helper::Promise<gin_helper::Dictionary> promise);
 
+#if BUILDFLAG(IS_LINUX)
+// Rewrite of SelectFileDialogLinuxPortal equivalent functions with primary
+// difference being that dbus_thread_linux::GetSharedSessionBus is not used
+// so that version detection can be initiated and compeleted on the dbus thread
+// Refs https://github.com/electron/electron/issues/46652
+void StartPortalAvailabilityTestInBackground();
+bool IsPortalAvailable();
+uint32_t GetPortalVersion();
+#endif
+
 }  // namespace file_dialog
 
 #endif  // ELECTRON_SHELL_BROWSER_UI_FILE_DIALOG_H_

filename: shell/browser/ui/file_dialog_linux.cc
patch: @@ -18,7 +18,6 @@
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/gin_helper/promise.h"
 #include "ui/shell_dialogs/select_file_dialog.h"
-#include "ui/shell_dialogs/select_file_dialog_linux_portal.h"
 #include "ui/shell_dialogs/select_file_policy.h"
 #include "ui/shell_dialogs/selected_file_info.h"
 
@@ -60,11 +59,9 @@ ui::SelectFileDialog::FileTypeInfo GetFilterInfo(const Filters& filters) {
 }
 
 void LogIfNeededAboutUnsupportedPortalFeature(const DialogSettings& settings) {
-  if (!settings.default_path.empty() &&
-      ui::SelectFileDialogLinuxPortal::IsPortalAvailable() &&
-      ui::SelectFileDialogLinuxPortal::GetPortalVersion() < 4) {
-    LOG(INFO) << "Available portal version "
-              << ui::SelectFileDialogLinuxPortal::GetPortalVersion()
+  if (!settings.default_path.empty() && IsPortalAvailable() &&
+      GetPortalVersion() < 4) {
+    LOG(INFO) << "Available portal version " << GetPortalVersion()
               << " does not support defaultPath option, try the non-portal"
               << " file chooser dialogs by launching with"
               << " --xdg-portal-required-version";

filename: shell/browser/ui/file_dialog_linux_portal.cc
patch: @@ -0,0 +1,121 @@
+// Copyright (c) 2025 Microsoft, GmbH.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/ui/file_dialog.h"
+
+#include <optional>
+
+#include "base/command_line.h"
+#include "base/functional/bind.h"
+#include "base/logging.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/no_destructor.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/synchronization/atomic_flag.h"
+#include "components/dbus/thread_linux/dbus_thread_linux.h"
+#include "components/dbus/utils/check_for_service_and_start.h"
+#include "dbus/bus.h"
+#include "dbus/object_path.h"
+#include "dbus/object_proxy.h"
+#include "dbus/property.h"
+
+namespace file_dialog {
+
+namespace {
+
+constexpr char kXdgPortalService[] = "org.freedesktop.portal.Desktop";
+constexpr char kXdgPortalObject[] = "/org/freedesktop/portal/desktop";
+constexpr char kFileChooserInterfaceName[] =
+    "org.freedesktop.portal.FileChooser";
+
+// Version 4 includes support for current_folder option to the OpenFile method
+// via https://github.com/flatpak/xdg-desktop-portal/commit/71165a5.
+uint32_t g_required_portal_version = 3;
+uint32_t g_available_portal_version = 0;
+constexpr char kXdgPortalRequiredVersionFlag[] = "xdg-portal-required-version";
+
+bool g_portal_available = false;
+
+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;
+};
+
+base::AtomicFlag* GetAvailabilityTestCompletionFlag() {
+  static base::NoDestructor<base::AtomicFlag> flag;
+  return flag.get();
+}
+
+void CheckPortalAvailabilityOnBusThread() {
+  auto* flag = GetAvailabilityTestCompletionFlag();
+  if (flag->IsSet())
+    return;
+
+  dbus::Bus::Options options;
+  options.bus_type = dbus::Bus::SESSION;
+  options.connection_type = dbus::Bus::PRIVATE;
+  options.dbus_task_runner = dbus_thread_linux::GetTaskRunner();
+  scoped_refptr<dbus::Bus> bus = base::MakeRefCounted<dbus::Bus>(options);
+  dbus_utils::CheckForServiceAndStart(
+      bus, kXdgPortalService,
+      base::BindOnce(
+          [](scoped_refptr<dbus::Bus> bus, base::AtomicFlag* flag,
+             std::optional<bool> name_has_owner) {
+            if (name_has_owner.value_or(false)) {
+              //
+              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() >=
+                         g_required_portal_version) {
+                g_portal_available = true;
+                g_available_portal_version = properties.version.value();
+              }
+            }
+            VLOG(1) << "File chooser portal available: "
+                    << (g_portal_available ? "yes" : "no");
+            flag->Set();
+            bus->ShutdownAndBlock();
+          },
+          std::move(bus), flag));
+}
+
+}  // namespace
+
+void StartPortalAvailabilityTestInBackground() {
+  if (GetAvailabilityTestCompletionFlag()->IsSet())
+    return;
+
+  const auto* cmd = base::CommandLine::ForCurrentProcess();
+  if (!base::StringToUint(
+          cmd->GetSwitchValueASCII(kXdgPortalRequiredVersionFlag),
+          &g_required_portal_version)) {
+    VLOG(1) << "Unable to parse --xdg-portal-required-version";
+  }
+
+  dbus_thread_linux::GetTaskRunner()->PostTask(
+      FROM_HERE, base::BindOnce(&CheckPortalAvailabilityOnBusThread));
+}
+
+bool IsPortalAvailable() {
+  if (!GetAvailabilityTestCompletionFlag()->IsSet())
+    LOG(WARNING) << "Portal availability checked before test was complete";
+
+  return g_portal_available;
+}
+
+uint32_t GetPortalVersion() {
+  return g_available_portal_version;
+}
+
+}  // namespace file_dialog