| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/shell_dialogs/select_file_dialog_linux_portal.h" |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.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 "base/task/sequenced_task_runner.h" |
| #include "base/time/time.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/gfx/native_widget_types.h" |
| #include "ui/linux/linux_ui_delegate.h" |
| #include "ui/shell_dialogs/select_file_dialog.h" |
| #include "ui/shell_dialogs/selected_file_info.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/widget/desktop_aura/desktop_window_tree_host_linux.h" |
| #include "url/gurl.h" |
| #include "url/url_util.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| constexpr char kDBusMethodNameHasOwner[] = "NameHasOwner"; |
| constexpr char kDBusMethodListActivatableNames[] = "ListActivatableNames"; |
| constexpr char kMethodStartServiceByName[] = "StartServiceByName"; |
| |
| 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 char kFileChooserOptionModal[] = "modal"; |
| |
| constexpr int kFileChooserFilterKindGlob = 0; |
| |
| constexpr char kFileUriPrefix[] = "file://"; |
| |
| // Time to wait for the notification service to start, in milliseconds. |
| constexpr base::TimeDelta kStartServiceTimeout = base::Seconds(1); |
| |
| 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( |
| base::make_span(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); |
| } |
| |
| scoped_refptr<dbus::Bus>* 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(); |
| } |
| |
| dbus::Bus* AcquireBusOnBusThread() { |
| return AcquireBusStorageOnBusThread()->get(); |
| } |
| |
| void 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(); |
| } |
| |
| } // namespace |
| |
| SelectFileDialogLinuxPortal::SelectFileDialogLinuxPortal( |
| Listener* listener, |
| std::unique_ptr<ui::SelectFilePolicy> policy) |
| : SelectFileDialogLinux(listener, std::move(policy)) {} |
| |
| SelectFileDialogLinuxPortal::~SelectFileDialogLinuxPortal() { |
| UnparentOnMainThread(); |
| // `info_` may have weak pointers which must be invalidated on the dbus |
| // thread. Pass our reference to that thread so weak pointers get invalidated |
| // on the correct sequence. |
| dbus_thread_linux::GetTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce([](scoped_refptr<DialogInfo> info) {}, std::move(info_))); |
| } |
| |
| // static |
| void SelectFileDialogLinuxPortal::StartAvailabilityTestInBackground() { |
| if (GetAvailabilityTestCompletionFlag()->IsSet()) |
| return; |
| |
| dbus_thread_linux::GetTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxPortal::CheckPortalAvailabilityOnBusThread)); |
| } |
| |
| // static |
| bool SelectFileDialogLinuxPortal::IsPortalAvailable() { |
| if (!GetAvailabilityTestCompletionFlag()->IsSet()) |
| LOG(WARNING) << "Portal availability checked before test was complete"; |
| |
| return is_portal_available_; |
| } |
| |
| // static |
| void SelectFileDialogLinuxPortal::DestroyPortalConnection() { |
| dbus_thread_linux::GetTaskRunner()->PostTask( |
| FROM_HERE, base::BindOnce(&DestroyBusOnBusThread)); |
| } |
| |
| bool SelectFileDialogLinuxPortal::IsRunning( |
| gfx::NativeWindow parent_window) const { |
| return parent_window && host_ && host_.get() == parent_window->GetHost(); |
| } |
| |
| void SelectFileDialogLinuxPortal::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, |
| const GURL* caller) { |
| info_ = base::MakeRefCounted<DialogInfo>( |
| base::BindOnce(&SelectFileDialogLinuxPortal::DialogCreatedOnMainThread, |
| weak_factory_.GetWeakPtr()), |
| base::BindOnce(&SelectFileDialogLinuxPortal::CompleteOpenOnMainThread, |
| weak_factory_.GetWeakPtr()), |
| base::BindOnce(&SelectFileDialogLinuxPortal::CancelOpenOnMainThread, |
| weak_factory_.GetWeakPtr())); |
| info_->type = type; |
| info_->main_task_runner = base::SequencedTaskRunner::GetCurrentDefault(); |
| listener_params_ = params; |
| |
| if (owning_window) { |
| if (auto* root = owning_window->GetRootWindow()) { |
| if (auto* host = root->GetNativeWindowProperty( |
| views::DesktopWindowTreeHostLinux::kWindowKey)) { |
| host_ = static_cast<aura::WindowTreeHost*>(host)->GetWeakPtr(); |
| } |
| } |
| } |
| |
| if (file_types) |
| set_file_types(*file_types); |
| |
| set_file_type_index(file_type_index); |
| |
| PortalFilterSet filter_set = BuildFilterSet(); |
| |
| // Keep a copy of the filters so the index of the chosen one can be identified |
| // and returned to listeners later. |
| filters_ = filter_set.filters; |
| |
| if (host_) { |
| auto* delegate = ui::LinuxUiDelegate::GetInstance(); |
| if (delegate && |
| delegate->ExportWindowHandle( |
| host_->GetAcceleratedWidget(), |
| base::BindOnce( |
| &SelectFileDialogLinuxPortal::SelectFileImplWithParentHandle, |
| this, title, default_path, filter_set, default_extension))) { |
| // Return early to skip the fallback below. |
| return; |
| } else { |
| LOG(WARNING) << "Failed to export window handle for portal select dialog"; |
| } |
| } |
| |
| // No parent, so just use a blank parent handle. |
| SelectFileImplWithParentHandle(title, default_path, filter_set, |
| default_extension, ""); |
| } |
| |
| bool SelectFileDialogLinuxPortal::HasMultipleFileTypeChoicesImpl() { |
| return file_types().extensions.size() > 1; |
| } |
| |
| // static |
| void SelectFileDialogLinuxPortal::CheckPortalAvailabilityOnBusThread() { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| 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 SelectFileDialogLinuxPortal::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) |
| .value_or(nullptr); |
| 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 SelectFileDialogLinuxPortal::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) |
| .value_or(nullptr); |
| 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; |
| } |
| |
| if (base::Contains(names, kXdgPortalService)) { |
| dbus::MethodCall start_service_call(DBUS_INTERFACE_DBUS, |
| kMethodStartServiceByName); |
| dbus::MessageWriter start_service_writer(&start_service_call); |
| start_service_writer.AppendString(kXdgPortalService); |
| start_service_writer.AppendUint32(/*flags=*/0); |
| auto start_service_response = |
| dbus_proxy |
| ->CallMethodAndBlock(&start_service_call, |
| kStartServiceTimeout.InMilliseconds()) |
| .value_or(nullptr); |
| if (!start_service_response) |
| return false; |
| dbus::MessageReader start_service_reader(start_service_response.get()); |
| uint32_t start_service_reply = 0; |
| if (start_service_reader.PopUint32(&start_service_reply) && |
| (start_service_reply == DBUS_START_REPLY_SUCCESS || |
| start_service_reply == DBUS_START_REPLY_ALREADY_RUNNING)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| SelectFileDialogLinuxPortal::PortalFilter::PortalFilter() = default; |
| SelectFileDialogLinuxPortal::PortalFilter::PortalFilter( |
| const PortalFilter& other) = default; |
| SelectFileDialogLinuxPortal::PortalFilter::PortalFilter(PortalFilter&& other) = |
| default; |
| SelectFileDialogLinuxPortal::PortalFilter::~PortalFilter() = default; |
| |
| SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet() = default; |
| SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet( |
| const PortalFilterSet& other) = default; |
| SelectFileDialogLinuxPortal::PortalFilterSet::PortalFilterSet( |
| PortalFilterSet&& other) = default; |
| SelectFileDialogLinuxPortal::PortalFilterSet::~PortalFilterSet() = default; |
| |
| SelectFileDialogLinuxPortal::DialogInfo::DialogInfo( |
| base::OnceClosure created_callback, |
| OnSelectFileExecutedCallback selected_callback, |
| OnSelectFileCanceledCallback canceled_callback) |
| : created_callback_(std::move(created_callback)), |
| selected_callback_(std::move(selected_callback)), |
| canceled_callback_(std::move(canceled_callback)) {} |
| SelectFileDialogLinuxPortal::DialogInfo::~DialogInfo() = default; |
| |
| // static |
| base::AtomicFlag* |
| SelectFileDialogLinuxPortal::GetAvailabilityTestCompletionFlag() { |
| static base::NoDestructor<base::AtomicFlag> flag; |
| return flag.get(); |
| } |
| |
| SelectFileDialogLinuxPortal::PortalFilterSet |
| SelectFileDialogLinuxPortal::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.push_back("*." + base::ToLowerASCII(extension)); |
| auto upper = "*." + base::ToUpperASCII(extension); |
| if (upper != filter.patterns.back()) |
| filter.patterns.push_back(std::move(upper)); |
| } |
| |
| 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]); |
| } |
| if (filter.name.empty()) { |
| std::vector<std::string> patterns_vector(filter.patterns.begin(), |
| filter.patterns.end()); |
| filter.name = base::JoinString(patterns_vector, ","); |
| } |
| |
| // The -1 is required to match against the right filter because |
| // |file_type_index_| is 1-indexed. |
| if (i == file_type_index() - 1) |
| 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.push_back("*.*"); |
| |
| filter_set.filters.push_back(std::move(filter)); |
| } |
| |
| return filter_set; |
| } |
| |
| void SelectFileDialogLinuxPortal::SelectFileImplWithParentHandle( |
| std::u16string title, |
| base::FilePath default_path, |
| PortalFilterSet filter_set, |
| base::FilePath::StringType default_extension, |
| std::string parent_handle) { |
| bool default_path_exists = CallDirectoryExistsOnUIThread(default_path); |
| dbus_thread_linux::GetTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &SelectFileDialogLinuxPortal::DialogInfo::SelectFileImplOnBusThread, |
| info_, std::move(title), std::move(default_path), default_path_exists, |
| std::move(filter_set), std::move(default_extension), |
| std::move(parent_handle))); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::SelectFileImplOnBusThread( |
| std::u16string title, |
| base::FilePath default_path, |
| const bool default_path_exists, |
| PortalFilterSet filter_set, |
| base::FilePath::StringType default_extension, |
| std::string parent_handle) { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| dbus::Bus* bus = AcquireBusOnBusThread(); |
| if (!bus->Connect()) |
| LOG(ERROR) << "Could not connect to bus for XDG portal"; |
| |
| std::string method; |
| switch (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 (type == SELECT_SAVEAS_FILE) { |
| message_id = IDS_SAVEAS_ALL_FILES; |
| } else if (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, response_handle_token, default_path, |
| default_path_exists, 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())); |
| |
| response_handle_ = |
| bus->GetObjectProxy(kXdgPortalService, expected_handle_path); |
| ConnectToHandle(); |
| |
| dbus::ObjectPath portal_path(kXdgPortalObject); |
| dbus::ObjectProxy* portal = |
| bus->GetObjectProxy(kXdgPortalService, portal_path); |
| portal->CallMethodWithErrorResponse( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, |
| base::BindOnce(&DialogInfo::OnCallResponse, weak_factory_.GetWeakPtr(), |
| base::Unretained(bus))); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::AppendOptions( |
| dbus::MessageWriter* writer, |
| const std::string& response_handle_token, |
| const base::FilePath& default_path, |
| const bool default_path_exists, |
| const SelectFileDialogLinuxPortal::PortalFilterSet& filter_set) { |
| dbus::MessageWriter options_writer(nullptr); |
| writer->OpenArray("{sv}", &options_writer); |
| |
| AppendStringOption(&options_writer, kFileChooserOptionHandleToken, |
| response_handle_token); |
| |
| if (type == SelectFileDialog::Type::SELECT_UPLOAD_FOLDER) { |
| AppendStringOption(&options_writer, kFileChooserOptionAcceptLabel, |
| l10n_util::GetStringUTF8( |
| IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON)); |
| } |
| |
| if (type == SelectFileDialog::Type::SELECT_FOLDER || |
| type == SelectFileDialog::Type::SELECT_UPLOAD_FOLDER || |
| type == SelectFileDialog::Type::SELECT_EXISTING_FOLDER) { |
| AppendBoolOption(&options_writer, kFileChooserOptionDirectory, true); |
| } else if (type == SelectFileDialog::Type::SELECT_OPEN_MULTI_FILE) { |
| AppendBoolOption(&options_writer, kFileChooserOptionMultiple, true); |
| } |
| |
| if (type == SelectFileDialog::Type::SELECT_SAVEAS_FILE && |
| !default_path.empty()) { |
| if (default_path_exists) { |
| // 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 SelectFileDialogLinuxGtk. |
| 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); |
| } |
| |
| AppendBoolOption(&options_writer, kFileChooserOptionModal, true); |
| |
| writer->CloseContainer(&options_writer); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::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 SelectFileDialogLinuxPortal::DialogInfo::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 SelectFileDialogLinuxPortal::DialogInfo::ConnectToHandle() { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| response_handle_->ConnectToSignal( |
| kXdgPortalRequestInterfaceName, kXdgPortalResponseSignal, |
| base::BindRepeating(&DialogInfo::OnResponseSignalEmitted, |
| weak_factory_.GetWeakPtr()), |
| base::BindOnce(&DialogInfo::OnResponseSignalConnected, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::CompleteOpen( |
| std::vector<base::FilePath> paths, |
| std::string current_filter) { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| response_handle_->Detach(); |
| main_task_runner->PostTask( |
| FROM_HERE, base::BindOnce(std::move(selected_callback_), std::move(paths), |
| std::move(current_filter))); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::CancelOpen() { |
| response_handle_->Detach(); |
| main_task_runner->PostTask(FROM_HERE, std::move(canceled_callback_)); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogCreatedOnMainThread() { |
| if (!host_) { |
| return; |
| } |
| host_->ReleaseCapture(); |
| reenable_window_event_handling_ = |
| static_cast<views::DesktopWindowTreeHostLinux*>(host_.get()) |
| ->DisableEventListening(); |
| } |
| |
| void SelectFileDialogLinuxPortal::CompleteOpenOnMainThread( |
| std::vector<base::FilePath> paths, |
| std::string current_filter) { |
| UnparentOnMainThread(); |
| |
| if (listener_) { |
| if (info_->type == SELECT_OPEN_MULTI_FILE) { |
| listener_->MultiFilesSelected(FilePathListToSelectedFileInfoList(paths), |
| listener_params_); |
| } else if (paths.size() > 1) { |
| LOG(ERROR) << "Got >1 file URI from a single-file chooser"; |
| } else { |
| int index = 1; |
| for (size_t i = 0; i < filters_.size(); ++i) { |
| if (filters_[i].name == current_filter) { |
| index = 1 + i; |
| break; |
| } |
| } |
| listener_->FileSelected(SelectedFileInfo(paths[0]), index, |
| listener_params_); |
| } |
| } |
| } |
| |
| void SelectFileDialogLinuxPortal::CancelOpenOnMainThread() { |
| UnparentOnMainThread(); |
| |
| if (listener_) |
| listener_->FileSelectionCanceled(listener_params_); |
| } |
| |
| void SelectFileDialogLinuxPortal::UnparentOnMainThread() { |
| if (reenable_window_event_handling_) { |
| std::move(reenable_window_event_handling_).Run(); |
| } |
| host_ = nullptr; |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::OnCallResponse( |
| dbus::Bus* bus, |
| dbus::Response* response, |
| dbus::ErrorResponse* error_response) { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| if (response) { |
| dbus::MessageReader reader(response); |
| dbus::ObjectPath actual_handle_path; |
| if (!reader.PopObjectPath(&actual_handle_path)) { |
| LOG(ERROR) << "Invalid portal response"; |
| } else { |
| if (response_handle_->object_path() != actual_handle_path) { |
| VLOG(1) << "Re-attaching response handle to " |
| << actual_handle_path.value(); |
| |
| response_handle_->Detach(); |
| response_handle_ = |
| bus->GetObjectProxy(kXdgPortalService, actual_handle_path); |
| ConnectToHandle(); |
| } |
| |
| // 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(); |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::OnResponseSignalConnected( |
| const std::string& interface, |
| const std::string& signal, |
| bool connected) { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| if (!connected) { |
| LOG(ERROR) << "Could not connect to Response signal"; |
| CancelOpen(); |
| } else if (created_callback_) { |
| main_task_runner->PostTask(FROM_HERE, std::move(created_callback_)); |
| } |
| } |
| |
| void SelectFileDialogLinuxPortal::DialogInfo::OnResponseSignalEmitted( |
| dbus::Signal* signal) { |
| DCHECK(dbus_thread_linux::GetTaskRunner()->RunsTasksInCurrentSequence()); |
| dbus::MessageReader reader(signal); |
| |
| std::vector<std::string> uris; |
| std::string current_filter; |
| if (!CheckResponseCode(&reader) || |
| !ReadResponseResults(&reader, &uris, ¤t_filter)) { |
| CancelOpen(); |
| return; |
| } |
| |
| std::vector<base::FilePath> paths = ConvertUrisToPaths(uris); |
| if (!paths.empty()) |
| CompleteOpen(std::move(paths), std::move(current_filter)); |
| else |
| CancelOpen(); |
| } |
| |
| bool SelectFileDialogLinuxPortal::DialogInfo::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 SelectFileDialogLinuxPortal::DialogInfo::ReadResponseResults( |
| dbus::MessageReader* reader, |
| std::vector<std::string>* uris, |
| std::string* current_filter) { |
| 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 <uris> response entry value"; |
| return false; |
| } |
| } |
| if (key == "current_filter") { |
| dbus::MessageReader current_filter_reader(nullptr); |
| dbus::MessageReader current_filter_struct_reader(nullptr); |
| if (!entry_reader.PopVariant(¤t_filter_reader) || |
| !current_filter_reader.PopStruct(¤t_filter_struct_reader) || |
| !current_filter_struct_reader.PopString(current_filter)) { |
| LOG(ERROR) << "Failed to read <current_filter> response entry value"; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| std::vector<base::FilePath> |
| SelectFileDialogLinuxPortal::DialogInfo::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, url::DecodeURLMode::kUTF8OrIsomorphic, &decoded_path); |
| paths.emplace_back(base::UTF16ToUTF8(decoded_path.view())); |
| } |
| |
| return paths; |
| } |
| |
| bool SelectFileDialogLinuxPortal::is_portal_available_ = false; |
| int SelectFileDialogLinuxPortal::handle_token_counter_ = 0; |
| |
| } // namespace ui |