From 1c2937a0afef75dde7e5654a13a53a64b9b094ff Mon Sep 17 00:00:00 2001 From: Samuel Pedotti Date: Thu, 17 Sep 2026 11:31:03 +0200 Subject: [PATCH 1/2] fix(share_plus): correct Windows share sheet data package setup The Windows share sheet failed with "We couldn't show you all the ways you could share" because the DataPackage handed to Windows was invalid. Several issues contributed to this: - IStorageFile was reinterpret_cast to IStorageItem. The two interfaces are unrelated at the ABI level, so the share target received a pointer with the wrong vtable. The interface is now obtained via QueryInterface. - The IStorageItem collection was stack allocated, while the DataPackage keeps a reference to it after the DataRequested handler returns. It is now heap allocated and owned by the plugin. - SetStorageItemsReadOnly was called unconditionally, so a text-only share advertised an empty file collection. It is now only called when there is at least one file. - An empty Title was set when sharing files without a title, subject or text. Windows rejects such a package, so the title now falls back to the subject, the text, the URI and finally the window title. - Files were resolved with a synchronous wait inside the DataRequested handler, which runs on the UI thread with a short deadline. They are now resolved before the share UI is shown. - add_DataRequested was called on every share without ever removing the previous handler, leaving stale handlers writing to later packages. Additionally, the plugin state is reset on every call so values are no longer inherited from the previous share, HRESULTs are propagated to Dart instead of always reporting success, the IStorageFile reference is no longer leaked, and Utf16FromUtf8 no longer embeds a NUL terminator in the converted string. --- .../share_plus/windows/share_plus_plugin.cpp | 396 ++++++++++++------ .../windows/share_plus_windows_plugin.h | 34 +- 2 files changed, 300 insertions(+), 130 deletions(-) diff --git a/packages/share_plus/share_plus/windows/share_plus_plugin.cpp b/packages/share_plus/share_plus/windows/share_plus_plugin.cpp index fed28d46cb..ff6fcca440 100644 --- a/packages/share_plus/share_plus/windows/share_plus_plugin.cpp +++ b/packages/share_plus/share_plus/windows/share_plus_plugin.cpp @@ -8,6 +8,54 @@ namespace share_plus_windows { +namespace { + +// Looks up |key| in |map| without inserting a null value for missing keys, +// which |std::map::operator[]| would do. +const flutter::EncodableValue *ValueOrNull(const flutter::EncodableMap &map, + const char *key) { + auto it = map.find(flutter::EncodableValue(key)); + if (it == map.end()) { + return nullptr; + } + return &it->second; +} + +std::optional StringOrNull(const flutter::EncodableMap &map, + const char *key) { + const auto *value = ValueOrNull(map, key); + if (value == nullptr) { + return std::nullopt; + } + if (const auto *string_value = std::get_if(value)) { + return *string_value; + } + return std::nullopt; +} + +std::vector StringListOrEmpty(const flutter::EncodableMap &map, + const char *key) { + std::vector result; + const auto *value = ValueOrNull(map, key); + if (value == nullptr) { + return result; + } + if (const auto *list = std::get_if(value)) { + for (const auto &entry : *list) { + if (const auto *string_value = std::get_if(&entry)) { + result.emplace_back(*string_value); + } + } + } + return result; +} + +bool HasValue(const std::optional &value) { + return value.has_value() && !value->empty(); +} + +} // namespace + void SharePlusWindowsPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) { auto channel = @@ -28,43 +76,59 @@ SharePlusWindowsPlugin::SharePlusWindowsPlugin( : registrar_(registrar) {} SharePlusWindowsPlugin::~SharePlusWindowsPlugin() { - if (data_transfer_manager_ != nullptr) { - data_transfer_manager_->remove_DataRequested(data_transfer_manager_token_); - data_transfer_manager_.Reset(); - } - if (data_transfer_manager_interop_ != nullptr) { - data_transfer_manager_interop_.Reset(); - } + RemoveDataRequestedHandler(); + data_transfer_manager_.Reset(); + data_transfer_manager_interop_.Reset(); + storage_items_.Reset(); } HWND SharePlusWindowsPlugin::GetWindow() { return ::GetAncestor(registrar_->GetView()->GetNativeWindow(), GA_ROOT); } +void SharePlusWindowsPlugin::RemoveDataRequestedHandler() { + if (has_data_transfer_manager_token_ && data_transfer_manager_ != nullptr) { + data_transfer_manager_->remove_DataRequested(data_transfer_manager_token_); + } + has_data_transfer_manager_token_ = false; + data_transfer_manager_token_ = {}; +} + WRL::ComPtr SharePlusWindowsPlugin::GetDataTransferManager() { using Microsoft::WRL::Wrappers::HStringReference; - ::RoGetActivationFactory( + // Drop the handler registered for the previous share request before the + // manager is replaced, otherwise it stays registered forever and every + // following request is served by several handlers at once. + RemoveDataRequestedHandler(); + data_transfer_manager_.Reset(); + data_transfer_manager_interop_.Reset(); + + HRESULT hr = ::RoGetActivationFactory( HStringReference( RuntimeClass_Windows_ApplicationModel_DataTransfer_DataTransferManager) .Get(), IID_PPV_ARGS(&data_transfer_manager_interop_)); - data_transfer_manager_interop_->GetForWindow( + if (FAILED(hr)) { + return nullptr; + } + hr = data_transfer_manager_interop_->GetForWindow( GetWindow(), IID_PPV_ARGS(&data_transfer_manager_)); + if (FAILED(hr)) { + data_transfer_manager_.Reset(); + } return data_transfer_manager_; } HRESULT SharePlusWindowsPlugin::GetStorageFileFromPath( - wchar_t *path, WindowsStorage::IStorageFile **file) { + const wchar_t *path, WindowsStorage::IStorageFile **file) { using Microsoft::WRL::Wrappers::HStringReference; WRL::ComPtr factory = nullptr; HRESULT hr = S_OK; *file = nullptr; - if (!factory) { - hr = WindowsFoundation::GetActivationFactory( - HStringReference(RuntimeClass_Windows_Storage_StorageFile).Get(), - &factory); - } + hr = WindowsFoundation::GetActivationFactory( + HStringReference(RuntimeClass_Windows_Storage_StorageFile).Get(), + &factory); if (SUCCEEDED(hr)) { WRL::ComPtr< WindowsFoundation::IAsyncOperation> @@ -82,7 +146,7 @@ HRESULT SharePlusWindowsPlugin::GetStorageFileFromPath( if (FAILED(hr) || status != AsyncStatus::Completed) { info->get_ErrorCode(&hr); } else { - async_operation->GetResults(file); + hr = async_operation->GetResults(file); } } } @@ -90,132 +154,212 @@ HRESULT SharePlusWindowsPlugin::GetStorageFileFromPath( return hr; } -void SharePlusWindowsPlugin::HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - // Handle the share method. - if (method_call.method_name().compare(kShare) == 0) { - auto data_transfer_manager = GetDataTransferManager(); - auto args = std::get(*method_call.arguments()); - - // Extract the text, subject, uri, title, paths and mimeTypes from the arguments - if (auto text_value = std::get_if( - &args[flutter::EncodableValue("text")])) { - share_text_ = *text_value; +HRESULT SharePlusWindowsPlugin::BuildStorageItems() { + storage_items_.Reset(); + storage_items_size_ = 0; + if (paths_.empty()) { + return S_OK; + } + + auto items = WRL::Make>(); + if (items == nullptr) { + return E_OUTOFMEMORY; + } + + for (const std::string &path : paths_) { + const auto wide_path = Utf16FromUtf8(path); + WRL::ComPtr file; + HRESULT hr = GetStorageFileFromPath(wide_path.c_str(), &file); + if (FAILED(hr) || file == nullptr) { + return FAILED(hr) ? hr : E_FAIL; } - if (auto subject_value = std::get_if( - &args[flutter::EncodableValue("subject")])) { - share_subject_ = *subject_value; + // |IStorageFile| does not derive from |IStorageItem| at the ABI level, so + // the interface has to be queried instead of cast. + WRL::ComPtr item; + hr = file.As(&item); + if (FAILED(hr)) { + return hr; } - if (auto uri_value = std::get_if( - &args[flutter::EncodableValue("uri")])) { - share_uri_ = *uri_value; + hr = items->Append(item.Get()); + if (FAILED(hr)) { + return hr; } - if (auto title_value = std::get_if( - &args[flutter::EncodableValue("title")])) { - share_title_ = *title_value; + ++storage_items_size_; + } + + return items.As(&storage_items_); +} + +std::wstring SharePlusWindowsPlugin::ResolveShareTitle() { + // Windows rejects a |DataPackage| without a title, which surfaces as + // "We couldn't show you all the ways you could share" in the share sheet. + if (HasValue(share_title_)) { + return Utf16FromUtf8(*share_title_); + } + if (HasValue(share_subject_)) { + return Utf16FromUtf8(*share_subject_); + } + if (HasValue(share_text_)) { + return Utf16FromUtf8(*share_text_); + } + if (HasValue(share_uri_)) { + return Utf16FromUtf8(*share_uri_); + } + // Sharing files only, with no title provided: fall back to the window title. + wchar_t window_title[256] = {}; + const int length = + ::GetWindowTextW(GetWindow(), window_title, ARRAYSIZE(window_title)); + if (length > 0) { + return std::wstring(window_title, static_cast(length)); + } + return L"Share"; +} + +HRESULT SharePlusWindowsPlugin::OnDataRequested( + DataTransfer::IDataRequestedEventArgs *e) { + using Microsoft::WRL::Wrappers::HStringReference; + WRL::ComPtr request; + HRESULT hr = e->get_Request(&request); + if (FAILED(hr)) { + return hr; + } + WRL::ComPtr data; + hr = request->get_Data(&data); + if (FAILED(hr)) { + return hr; + } + WRL::ComPtr properties; + hr = data->get_Properties(&properties); + if (FAILED(hr)) { + return hr; + } + + const auto title = ResolveShareTitle(); + hr = properties->put_Title(HStringReference(title.c_str()).Get()); + if (FAILED(hr)) { + return hr; + } + + // Prefer the URI over the text when both are present, matching the other + // platform implementations. + std::wstring body; + if (HasValue(share_uri_)) { + body = Utf16FromUtf8(*share_uri_); + } else if (HasValue(share_text_)) { + body = Utf16FromUtf8(*share_text_); + } + if (!body.empty()) { + properties->put_Description(HStringReference(body.c_str()).Get()); + hr = data->SetText(HStringReference(body.c_str()).Get()); + if (FAILED(hr)) { + return hr; } - if (auto paths = std::get_if( - &args[flutter::EncodableValue("paths")])) { - paths_.clear(); - for (auto& path : *paths) { - paths_.emplace_back(std::get(path)); - } + } + + // Only set storage items when there is at least one: an empty collection + // makes the share sheet fail to enumerate share targets. + if (storage_items_ != nullptr && storage_items_size_ > 0) { + WRL::ComPtr> + iterable; + hr = storage_items_.As(&iterable); + if (FAILED(hr)) { + return hr; } - if (auto mime_types = std::get_if( - &args[flutter::EncodableValue("mimeTypes")])) { - mime_types_.clear(); - for (auto& mime_type : *mime_types) { - mime_types_.emplace_back(std::get(mime_type)); - } + hr = data->SetStorageItemsReadOnly(iterable.Get()); + if (FAILED(hr)) { + return hr; } + } - // Create the share callback - auto callback = WRL::Callback>( - [&](auto &&, DataTransfer::IDataRequestedEventArgs *e) { - using Microsoft::WRL::Wrappers::HStringReference; - WRL::ComPtr request; - e->get_Request(&request); - WRL::ComPtr data; - request->get_Data(&data); - WRL::ComPtr properties; - data->get_Properties(&properties); - - // Set the title of the share dialog - // Prefer the title, then the subject, then the text - // Setting a title is mandatory for Windows - if (share_title_ && !share_title_.value_or("").empty()) { - auto title = Utf16FromUtf8(share_title_.value_or("")); - properties->put_Title(HStringReference(title.c_str()).Get()); - } - else if (share_subject_ && !share_subject_.value_or("").empty()) { - auto title = Utf16FromUtf8(share_subject_.value_or("")); - properties->put_Title(HStringReference(title.c_str()).Get()); - } - else { - auto title = Utf16FromUtf8(share_text_.value_or("")); - properties->put_Title(HStringReference(title.c_str()).Get()); - } - - // Set the text of the share dialog - if (share_text_ && !share_text_.value_or("").empty()) { - auto text = Utf16FromUtf8(share_text_.value_or("")); - properties->put_Description( - HStringReference(text.c_str()).Get()); - data->SetText(HStringReference(text.c_str()).Get()); - } - - // If URI provided, set the URI to share - if (share_uri_ && !share_uri_.value_or("").empty()) { - auto uri = Utf16FromUtf8(share_uri_.value_or("")); - properties->put_Description( - HStringReference(uri.c_str()).Get()); - data->SetText(HStringReference(uri.c_str()).Get()); - } - - // Add files to the data. - Vector storage_items; - for (const std::string& path : paths_) { - auto str = Utf16FromUtf8(path); - wchar_t* ptr = const_cast(str.c_str()); - WindowsStorage::IStorageFile* file = nullptr; - if (SUCCEEDED(GetStorageFileFromPath(ptr, &file)) && - file != nullptr) { - storage_items.Append( - reinterpret_cast(file)); - } - } - data->SetStorageItemsReadOnly(&storage_items); - - return S_OK; - }); - - // Add the callback to the data transfer manager - data_transfer_manager->add_DataRequested(callback.Get(), - &data_transfer_manager_token_); - if (data_transfer_manager_interop_ != nullptr) { - data_transfer_manager_interop_->ShowShareUIForWindow(GetWindow()); - } - result->Success(flutter::EncodableValue(kShareResultUnavailable)); - } else { + return S_OK; +} + +void SharePlusWindowsPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) { + // Handle the share method. + if (method_call.method_name().compare(kShare) != 0) { result->NotImplemented(); + return; } + + const auto *args = + std::get_if(method_call.arguments()); + if (args == nullptr) { + result->Error("share_plus", "Missing or invalid arguments."); + return; + } + + // Reset every field: values missing from this call must not be inherited + // from the previous one. + share_text_ = StringOrNull(*args, "text"); + share_subject_ = StringOrNull(*args, "subject"); + share_uri_ = StringOrNull(*args, "uri"); + share_title_ = StringOrNull(*args, "title"); + paths_ = StringListOrEmpty(*args, "paths"); + mime_types_ = StringListOrEmpty(*args, "mimeTypes"); + + // Resolve the files before the share UI is shown. The |DataRequested| + // handler runs on the UI thread with a short deadline, and the synchronous + // wait performed here would make it time out. + HRESULT hr = BuildStorageItems(); + if (FAILED(hr)) { + result->Error("share_plus", "Failed to open the files to share."); + return; + } + + auto data_transfer_manager = GetDataTransferManager(); + if (data_transfer_manager == nullptr || + data_transfer_manager_interop_ == nullptr) { + result->Error("share_plus", "DataTransferManager is not available."); + return; + } + + auto callback = WRL::Callback>( + [this](DataTransfer::IDataTransferManager *, + DataTransfer::IDataRequestedEventArgs *e) -> HRESULT { + return OnDataRequested(e); + }); + + hr = data_transfer_manager->add_DataRequested(callback.Get(), + &data_transfer_manager_token_); + if (FAILED(hr)) { + result->Error("share_plus", "Failed to register the share handler."); + return; + } + has_data_transfer_manager_token_ = true; + + hr = data_transfer_manager_interop_->ShowShareUIForWindow(GetWindow()); + if (FAILED(hr)) { + RemoveDataRequestedHandler(); + result->Error("share_plus", "Failed to show the share UI."); + return; + } + + result->Success(flutter::EncodableValue(kShareResultUnavailable)); } // Converts string encoded in UTF-8 to wstring. // Returns an empty |std::wstring| on failure. // Present as static helper method. -std::wstring SharePlusWindowsPlugin::Utf16FromUtf8(std::string string) { - int size_needed = - MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, NULL, 0); - if (size_needed == 0) { +std::wstring SharePlusWindowsPlugin::Utf16FromUtf8(const std::string &string) { + if (string.empty()) { + return std::wstring(); + } + // |string.size()| excludes the terminator on purpose: including it would + // embed a NUL in the result, which WinRT strings carry along. + const int size_needed = MultiByteToWideChar( + CP_UTF8, 0, string.c_str(), static_cast(string.size()), nullptr, 0); + if (size_needed <= 0) { return std::wstring(); } - std::wstring result(size_needed, 0); - int converted_length = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), -1, - &result[0], size_needed); + std::wstring result(static_cast(size_needed), 0); + const int converted_length = MultiByteToWideChar( + CP_UTF8, 0, string.c_str(), static_cast(string.size()), &result[0], + size_needed); if (converted_length == 0) { return std::wstring(); } diff --git a/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h b/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h index ca7d2aaa14..8fe1c982be 100644 --- a/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h +++ b/packages/share_plus/share_plus/windows/share_plus_windows_plugin.h @@ -17,6 +17,10 @@ #include #include +#include +#include +#include + #pragma comment(lib, "runtimeobject.lib") namespace WRL = Microsoft::WRL; @@ -45,27 +49,42 @@ class SharePlusWindowsPlugin : public flutter::Plugin { "dev.fluttercommunity.plus/share/unavailable"; static constexpr auto kShare = "share"; - //static constexpr auto kShareFiles = "shareFiles"; HWND GetWindow(); WRL::ComPtr GetDataTransferManager(); + // Unregisters the |DataRequested| handler registered by a previous share + // call, if any. Not doing so leaks handlers that keep writing to the + // |DataPackage| of subsequent share requests. + void RemoveDataRequestedHandler(); + void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); - static HRESULT GetStorageFileFromPath(wchar_t *path, + // Resolves |paths_| into |storage_items_|. Done before the share UI is shown + // because the |DataRequested| handler must return quickly. + HRESULT BuildStorageItems(); + + HRESULT OnDataRequested(DataTransfer::IDataRequestedEventArgs *args); + + // Title shown by the share sheet. Windows fails the whole request when the + // |DataPackage| has an empty title, so this always returns a non-empty value. + std::wstring ResolveShareTitle(); + + static HRESULT GetStorageFileFromPath(const wchar_t *path, WindowsStorage::IStorageFile **file); - static std::wstring SharePlusWindowsPlugin::Utf16FromUtf8(std::string string); + static std::wstring Utf16FromUtf8(const std::string &string); flutter::PluginRegistrarWindows *registrar_ = nullptr; WRL::ComPtr data_transfer_manager_interop_ = nullptr; WRL::ComPtr data_transfer_manager_ = nullptr; - EventRegistrationToken data_transfer_manager_token_; + EventRegistrationToken data_transfer_manager_token_ = {}; + bool has_data_transfer_manager_token_ = false; // Present here to keep |std::string| in memory until data request callback // from |IDataTransferManager| takes place. @@ -76,6 +95,13 @@ class SharePlusWindowsPlugin : public flutter::Plugin { std::optional share_title_ = std::nullopt; std::vector paths_ = {}; std::vector mime_types_ = {}; + + // Heap allocated and ref-counted: the |DataPackage| keeps a reference to this + // collection well after the |DataRequested| handler has returned. + WRL::ComPtr< + WindowsFoundation::Collections::IVector> + storage_items_ = nullptr; + unsigned storage_items_size_ = 0; }; } // namespace share_plus_windows From 106bbec2650066867c9f33fe380ec4b0f4b20e80 Mon Sep 17 00:00:00 2001 From: Samuel Pedotti Date: Thu, 17 Sep 2026 11:42:55 +0200 Subject: [PATCH 2/2] chore(share_plus): apply dart format to share_plus_windows_test.dart The file was not formatted according to the dart format version used by CI, which made the formatting check report it. It is unrelated to the Windows fix in this PR. --- .../test/share_plus_windows_test.dart | 54 ++++++++----------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/packages/share_plus/share_plus/test/share_plus_windows_test.dart b/packages/share_plus/share_plus/test/share_plus_windows_test.dart index 52017903bf..f76eaafce9 100644 --- a/packages/share_plus/share_plus/test/share_plus_windows_test.dart +++ b/packages/share_plus/share_plus/test/share_plus_windows_test.dart @@ -19,46 +19,34 @@ void main() { // These tests are only valid on Windows versions lower than 10.0.17763.0. - test( - 'url encoding is correct for &', - () async { - final mock = MockUrlLauncherPlatform(); + test('url encoding is correct for &', () async { + final mock = MockUrlLauncherPlatform(); - await SharePlusWindowsPlugin( - mock, - ).share(ShareParams(text: 'foo&bar', subject: 'bar&foo')); + await SharePlusWindowsPlugin( + mock, + ).share(ShareParams(text: 'foo&bar', subject: 'bar&foo')); - expect(mock.url, 'mailto:?subject=bar%26foo&body=foo%26bar'); - }, - skip: VersionHelper.instance.isWindows10RS5OrGreater, - ); + expect(mock.url, 'mailto:?subject=bar%26foo&body=foo%26bar'); + }, skip: VersionHelper.instance.isWindows10RS5OrGreater); // see https://github.com/dart-lang/sdk/issues/43838#issuecomment-823551891 - test( - 'url encoding is correct for spaces', - () async { - final mock = MockUrlLauncherPlatform(); + test('url encoding is correct for spaces', () async { + final mock = MockUrlLauncherPlatform(); - await SharePlusWindowsPlugin( - mock, - ).share(ShareParams(text: 'foo bar', subject: 'bar foo')); + await SharePlusWindowsPlugin( + mock, + ).share(ShareParams(text: 'foo bar', subject: 'bar foo')); - expect(mock.url, 'mailto:?subject=bar%20foo&body=foo%20bar'); - }, - skip: VersionHelper.instance.isWindows10RS5OrGreater, - ); + expect(mock.url, 'mailto:?subject=bar%20foo&body=foo%20bar'); + }, skip: VersionHelper.instance.isWindows10RS5OrGreater); - test( - 'can share URI on Windows', - () async { - final mock = MockUrlLauncherPlatform(); + test('can share URI on Windows', () async { + final mock = MockUrlLauncherPlatform(); - await SharePlusWindowsPlugin( - mock, - ).share(ShareParams(uri: Uri.parse('http://example.com'))); + await SharePlusWindowsPlugin( + mock, + ).share(ShareParams(uri: Uri.parse('http://example.com'))); - expect(mock.url, 'mailto:?body=http%3A%2F%2Fexample.com'); - }, - skip: VersionHelper.instance.isWindows10RS5OrGreater, - ); + expect(mock.url, 'mailto:?body=http%3A%2F%2Fexample.com'); + }, skip: VersionHelper.instance.isWindows10RS5OrGreater); }