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); } 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