| // Copyright 2013 The Flutter 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 "flutter/shell/platform/windows/platform_handler.h" | |
| #include <windows.h> | |
| #include <cstring> | |
| #include <optional> | |
| #include "flutter/fml/logging.h" | |
| #include "flutter/fml/macros.h" | |
| #include "flutter/fml/platform/win/wstring_conversion.h" | |
| #include "flutter/shell/platform/common/client_wrapper/include/flutter/method_result_functions.h" | |
| #include "flutter/shell/platform/common/json_method_codec.h" | |
| #include "flutter/shell/platform/windows/flutter_windows_view.h" | |
| static constexpr char kChannelName[] = "flutter/platform"; | |
| static constexpr char kGetClipboardDataMethod[] = "Clipboard.getData"; | |
| static constexpr char kHasStringsClipboardMethod[] = "Clipboard.hasStrings"; | |
| static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData"; | |
| static constexpr char kExitApplicationMethod[] = "System.exitApplication"; | |
| static constexpr char kRequestAppExitMethod[] = "System.requestAppExit"; | |
| static constexpr char kInitializationCompleteMethod[] = | |
| "System.initializationComplete"; | |
| static constexpr char kPlaySoundMethod[] = "SystemSound.play"; | |
| static constexpr char kExitCodeKey[] = "exitCode"; | |
| static constexpr char kExitTypeKey[] = "type"; | |
| static constexpr char kExitResponseKey[] = "response"; | |
| static constexpr char kExitResponseCancel[] = "cancel"; | |
| static constexpr char kExitResponseExit[] = "exit"; | |
| static constexpr char kTextPlainFormat[] = "text/plain"; | |
| static constexpr char kTextKey[] = "text"; | |
| static constexpr char kUnknownClipboardFormatMessage[] = | |
| "Unknown clipboard format"; | |
| static constexpr char kValueKey[] = "value"; | |
| static constexpr int kAccessDeniedErrorCode = 5; | |
| static constexpr int kErrorSuccess = 0; | |
| static constexpr char kExitRequestError[] = "ExitApplication error"; | |
| static constexpr char kInvalidExitRequestMessage[] = | |
| "Invalid application exit request"; | |
| namespace flutter { | |
| namespace { | |
| // A scoped wrapper for GlobalAlloc/GlobalFree. | |
| class ScopedGlobalMemory { | |
| public: | |
| // Allocates |bytes| bytes of global memory with the given flags. | |
| ScopedGlobalMemory(unsigned int flags, size_t bytes) { | |
| memory_ = ::GlobalAlloc(flags, bytes); | |
| if (!memory_) { | |
| FML_LOG(ERROR) << "Unable to allocate global memory: " | |
| << ::GetLastError(); | |
| } | |
| } | |
| ~ScopedGlobalMemory() { | |
| if (memory_) { | |
| if (::GlobalFree(memory_) != nullptr) { | |
| FML_LOG(ERROR) << "Failed to free global allocation: " | |
| << ::GetLastError(); | |
| } | |
| } | |
| } | |
| // Returns the memory pointer, which will be nullptr if allocation failed. | |
| void* get() { return memory_; } | |
| void* release() { | |
| void* memory = memory_; | |
| memory_ = nullptr; | |
| return memory; | |
| } | |
| private: | |
| HGLOBAL memory_; | |
| FML_DISALLOW_COPY_AND_ASSIGN(ScopedGlobalMemory); | |
| }; | |
| // A scoped wrapper for GlobalLock/GlobalUnlock. | |
| class ScopedGlobalLock { | |
| public: | |
| // Attempts to acquire a global lock on |memory| for the life of this object. | |
| ScopedGlobalLock(HGLOBAL memory) { | |
| source_ = memory; | |
| if (memory) { | |
| locked_memory_ = ::GlobalLock(memory); | |
| if (!locked_memory_) { | |
| FML_LOG(ERROR) << "Unable to acquire global lock: " << ::GetLastError(); | |
| } | |
| } | |
| } | |
| ~ScopedGlobalLock() { | |
| if (locked_memory_) { | |
| if (!::GlobalUnlock(source_)) { | |
| DWORD error = ::GetLastError(); | |
| if (error != NO_ERROR) { | |
| FML_LOG(ERROR) << "Unable to release global lock: " | |
| << ::GetLastError(); | |
| } | |
| } | |
| } | |
| } | |
| // Returns the locked memory pointer, which will be nullptr if acquiring the | |
| // lock failed. | |
| void* get() { return locked_memory_; } | |
| private: | |
| HGLOBAL source_; | |
| void* locked_memory_; | |
| FML_DISALLOW_COPY_AND_ASSIGN(ScopedGlobalLock); | |
| }; | |
| // A Clipboard wrapper that automatically closes the clipboard when it goes out | |
| // of scope. | |
| class ScopedClipboard : public ScopedClipboardInterface { | |
| public: | |
| ScopedClipboard(); | |
| virtual ~ScopedClipboard(); | |
| int Open(HWND window) override; | |
| bool HasString() override; | |
| std::variant<std::wstring, int> GetString() override; | |
| int SetString(const std::wstring string) override; | |
| private: | |
| bool opened_ = false; | |
| FML_DISALLOW_COPY_AND_ASSIGN(ScopedClipboard); | |
| }; | |
| ScopedClipboard::ScopedClipboard() {} | |
| ScopedClipboard::~ScopedClipboard() { | |
| if (opened_) { | |
| ::CloseClipboard(); | |
| } | |
| } | |
| int ScopedClipboard::Open(HWND window) { | |
| opened_ = ::OpenClipboard(window); | |
| if (!opened_) { | |
| return ::GetLastError(); | |
| } | |
| return kErrorSuccess; | |
| } | |
| bool ScopedClipboard::HasString() { | |
| // Allow either plain text format, since getting data will auto-interpolate. | |
| return ::IsClipboardFormatAvailable(CF_UNICODETEXT) || | |
| ::IsClipboardFormatAvailable(CF_TEXT); | |
| } | |
| std::variant<std::wstring, int> ScopedClipboard::GetString() { | |
| FML_DCHECK(opened_) << "Called GetString when clipboard is closed"; | |
| HANDLE data = ::GetClipboardData(CF_UNICODETEXT); | |
| if (data == nullptr) { | |
| return ::GetLastError(); | |
| } | |
| ScopedGlobalLock locked_data(data); | |
| if (!locked_data.get()) { | |
| return ::GetLastError(); | |
| } | |
| return static_cast<wchar_t*>(locked_data.get()); | |
| } | |
| int ScopedClipboard::SetString(const std::wstring string) { | |
| FML_DCHECK(opened_) << "Called GetString when clipboard is closed"; | |
| if (!::EmptyClipboard()) { | |
| return ::GetLastError(); | |
| } | |
| size_t null_terminated_byte_count = | |
| sizeof(decltype(string)::traits_type::char_type) * (string.size() + 1); | |
| ScopedGlobalMemory destination_memory(GMEM_MOVEABLE, | |
| null_terminated_byte_count); | |
| ScopedGlobalLock locked_memory(destination_memory.get()); | |
| if (!locked_memory.get()) { | |
| return ::GetLastError(); | |
| } | |
| memcpy(locked_memory.get(), string.c_str(), null_terminated_byte_count); | |
| if (!::SetClipboardData(CF_UNICODETEXT, locked_memory.get())) { | |
| return ::GetLastError(); | |
| } | |
| // The clipboard now owns the global memory. | |
| destination_memory.release(); | |
| return kErrorSuccess; | |
| } | |
| } // namespace | |
| static AppExitType StringToAppExitType(const std::string& string) { | |
| if (string.compare(PlatformHandler::kExitTypeRequired) == 0) { | |
| return AppExitType::required; | |
| } else if (string.compare(PlatformHandler::kExitTypeCancelable) == 0) { | |
| return AppExitType::cancelable; | |
| } | |
| FML_LOG(ERROR) << string << " is not recognized as a valid exit type."; | |
| return AppExitType::required; | |
| } | |
| PlatformHandler::PlatformHandler( | |
| BinaryMessenger* messenger, | |
| FlutterWindowsEngine* engine, | |
| std::optional<std::function<std::unique_ptr<ScopedClipboardInterface>()>> | |
| scoped_clipboard_provider) | |
| : channel_(std::make_unique<MethodChannel<rapidjson::Document>>( | |
| messenger, | |
| kChannelName, | |
| &JsonMethodCodec::GetInstance())), | |
| engine_(engine) { | |
| channel_->SetMethodCallHandler( | |
| [this](const MethodCall<rapidjson::Document>& call, | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| HandleMethodCall(call, std::move(result)); | |
| }); | |
| if (scoped_clipboard_provider.has_value()) { | |
| scoped_clipboard_provider_ = scoped_clipboard_provider.value(); | |
| } else { | |
| scoped_clipboard_provider_ = []() { | |
| return std::make_unique<ScopedClipboard>(); | |
| }; | |
| } | |
| } | |
| PlatformHandler::~PlatformHandler() = default; | |
| void PlatformHandler::GetPlainText( | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result, | |
| std::string_view key) { | |
| // TODO(loicsharma): Remove implicit view assumption. | |
| // https://github.com/flutter/flutter/issues/142845 | |
| const FlutterWindowsView* view = engine_->view(kImplicitViewId); | |
| if (view == nullptr) { | |
| result->Error(kClipboardError, | |
| "Clipboard is not available in Windows headless mode"); | |
| return; | |
| } | |
| std::unique_ptr<ScopedClipboardInterface> clipboard = | |
| scoped_clipboard_provider_(); | |
| int open_result = clipboard->Open(view->GetWindowHandle()); | |
| if (open_result != kErrorSuccess) { | |
| rapidjson::Document error_code; | |
| error_code.SetInt(open_result); | |
| result->Error(kClipboardError, "Unable to open clipboard", error_code); | |
| return; | |
| } | |
| if (!clipboard->HasString()) { | |
| result->Success(rapidjson::Document()); | |
| return; | |
| } | |
| std::variant<std::wstring, int> get_string_result = clipboard->GetString(); | |
| if (std::holds_alternative<int>(get_string_result)) { | |
| rapidjson::Document error_code; | |
| error_code.SetInt(std::get<int>(get_string_result)); | |
| result->Error(kClipboardError, "Unable to get clipboard data", error_code); | |
| return; | |
| } | |
| rapidjson::Document document; | |
| document.SetObject(); | |
| rapidjson::Document::AllocatorType& allocator = document.GetAllocator(); | |
| document.AddMember( | |
| rapidjson::Value(key.data(), allocator), | |
| rapidjson::Value( | |
| fml::WideStringToUtf8(std::get<std::wstring>(get_string_result)), | |
| allocator), | |
| allocator); | |
| result->Success(document); | |
| } | |
| void PlatformHandler::GetHasStrings( | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| // TODO(loicsharma): Remove implicit view assumption. | |
| // https://github.com/flutter/flutter/issues/142845 | |
| const FlutterWindowsView* view = engine_->view(kImplicitViewId); | |
| if (view == nullptr) { | |
| result->Error(kClipboardError, | |
| "Clipboard is not available in Windows headless mode"); | |
| return; | |
| } | |
| std::unique_ptr<ScopedClipboardInterface> clipboard = | |
| scoped_clipboard_provider_(); | |
| bool hasStrings; | |
| int open_result = clipboard->Open(view->GetWindowHandle()); | |
| if (open_result != kErrorSuccess) { | |
| // Swallow errors of type ERROR_ACCESS_DENIED. These happen when the app is | |
| // not in the foreground and GetHasStrings is irrelevant. | |
| // See https://github.com/flutter/flutter/issues/95817. | |
| if (open_result != kAccessDeniedErrorCode) { | |
| rapidjson::Document error_code; | |
| error_code.SetInt(open_result); | |
| result->Error(kClipboardError, "Unable to open clipboard", error_code); | |
| return; | |
| } | |
| hasStrings = false; | |
| } else { | |
| hasStrings = clipboard->HasString(); | |
| } | |
| rapidjson::Document document; | |
| document.SetObject(); | |
| rapidjson::Document::AllocatorType& allocator = document.GetAllocator(); | |
| document.AddMember(rapidjson::Value(kValueKey, allocator), | |
| rapidjson::Value(hasStrings), allocator); | |
| result->Success(document); | |
| } | |
| void PlatformHandler::SetPlainText( | |
| const std::string& text, | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| // TODO(loicsharma): Remove implicit view assumption. | |
| // https://github.com/flutter/flutter/issues/142845 | |
| const FlutterWindowsView* view = engine_->view(kImplicitViewId); | |
| if (view == nullptr) { | |
| result->Error(kClipboardError, | |
| "Clipboard is not available in Windows headless mode"); | |
| return; | |
| } | |
| std::unique_ptr<ScopedClipboardInterface> clipboard = | |
| scoped_clipboard_provider_(); | |
| int open_result = clipboard->Open(view->GetWindowHandle()); | |
| if (open_result != kErrorSuccess) { | |
| rapidjson::Document error_code; | |
| error_code.SetInt(open_result); | |
| result->Error(kClipboardError, "Unable to open clipboard", error_code); | |
| return; | |
| } | |
| int set_result = clipboard->SetString(fml::Utf8ToWideString(text)); | |
| if (set_result != kErrorSuccess) { | |
| rapidjson::Document error_code; | |
| error_code.SetInt(set_result); | |
| result->Error(kClipboardError, "Unable to set clipboard data", error_code); | |
| return; | |
| } | |
| result->Success(); | |
| } | |
| void PlatformHandler::SystemSoundPlay( | |
| const std::string& sound_type, | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| if (sound_type.compare(kSoundTypeAlert) == 0) { | |
| MessageBeep(MB_OK); | |
| result->Success(); | |
| } else { | |
| result->NotImplemented(); | |
| } | |
| } | |
| void PlatformHandler::SystemExitApplication( | |
| AppExitType exit_type, | |
| UINT exit_code, | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| rapidjson::Document result_doc; | |
| result_doc.SetObject(); | |
| if (exit_type == AppExitType::required) { | |
| QuitApplication(std::nullopt, std::nullopt, std::nullopt, exit_code); | |
| result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseExit, | |
| result_doc.GetAllocator()); | |
| result->Success(result_doc); | |
| } else { | |
| RequestAppExit(std::nullopt, std::nullopt, std::nullopt, exit_type, | |
| exit_code); | |
| result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseCancel, | |
| result_doc.GetAllocator()); | |
| result->Success(result_doc); | |
| } | |
| } | |
| // Indicates whether an exit request may be canceled by the framework. | |
| // These values must be kept in sync with ExitType in platform_handler.h | |
| static constexpr const char* kExitTypeNames[] = { | |
| PlatformHandler::kExitTypeRequired, PlatformHandler::kExitTypeCancelable}; | |
| void PlatformHandler::RequestAppExit(std::optional<HWND> hwnd, | |
| std::optional<WPARAM> wparam, | |
| std::optional<LPARAM> lparam, | |
| AppExitType exit_type, | |
| UINT exit_code) { | |
| auto callback = std::make_unique<MethodResultFunctions<rapidjson::Document>>( | |
| [this, exit_code, hwnd, wparam, | |
| lparam](const rapidjson::Document* response) { | |
| RequestAppExitSuccess(hwnd, wparam, lparam, response, exit_code); | |
| }, | |
| nullptr, nullptr); | |
| auto args = std::make_unique<rapidjson::Document>(); | |
| args->SetObject(); | |
| args->GetObjectW().AddMember( | |
| kExitTypeKey, std::string(kExitTypeNames[static_cast<int>(exit_type)]), | |
| args->GetAllocator()); | |
| channel_->InvokeMethod(kRequestAppExitMethod, std::move(args), | |
| std::move(callback)); | |
| } | |
| void PlatformHandler::RequestAppExitSuccess(std::optional<HWND> hwnd, | |
| std::optional<WPARAM> wparam, | |
| std::optional<LPARAM> lparam, | |
| const rapidjson::Document* result, | |
| UINT exit_code) { | |
| rapidjson::Value::ConstMemberIterator itr = | |
| result->FindMember(kExitResponseKey); | |
| if (itr == result->MemberEnd() || !itr->value.IsString()) { | |
| FML_LOG(ERROR) << "Application request response did not contain a valid " | |
| "response value"; | |
| return; | |
| } | |
| const std::string& exit_type = itr->value.GetString(); | |
| if (exit_type.compare(kExitResponseExit) == 0) { | |
| QuitApplication(hwnd, wparam, lparam, exit_code); | |
| } | |
| } | |
| void PlatformHandler::QuitApplication(std::optional<HWND> hwnd, | |
| std::optional<WPARAM> wparam, | |
| std::optional<LPARAM> lparam, | |
| UINT exit_code) { | |
| engine_->OnQuit(hwnd, wparam, lparam, exit_code); | |
| } | |
| void PlatformHandler::HandleMethodCall( | |
| const MethodCall<rapidjson::Document>& method_call, | |
| std::unique_ptr<MethodResult<rapidjson::Document>> result) { | |
| const std::string& method = method_call.method_name(); | |
| if (method.compare(kExitApplicationMethod) == 0) { | |
| const rapidjson::Value& arguments = method_call.arguments()[0]; | |
| rapidjson::Value::ConstMemberIterator itr = | |
| arguments.FindMember(kExitTypeKey); | |
| if (itr == arguments.MemberEnd() || !itr->value.IsString()) { | |
| result->Error(kExitRequestError, kInvalidExitRequestMessage); | |
| return; | |
| } | |
| const std::string& exit_type = itr->value.GetString(); | |
| itr = arguments.FindMember(kExitCodeKey); | |
| if (itr == arguments.MemberEnd() || !itr->value.IsInt()) { | |
| result->Error(kExitRequestError, kInvalidExitRequestMessage); | |
| return; | |
| } | |
| UINT exit_code = arguments[kExitCodeKey].GetInt(); | |
| SystemExitApplication(StringToAppExitType(exit_type), exit_code, | |
| std::move(result)); | |
| } else if (method.compare(kGetClipboardDataMethod) == 0) { | |
| // Only one string argument is expected. | |
| const rapidjson::Value& format = method_call.arguments()[0]; | |
| if (strcmp(format.GetString(), kTextPlainFormat) != 0) { | |
| result->Error(kClipboardError, kUnknownClipboardFormatMessage); | |
| return; | |
| } | |
| GetPlainText(std::move(result), kTextKey); | |
| } else if (method.compare(kHasStringsClipboardMethod) == 0) { | |
| // Only one string argument is expected. | |
| const rapidjson::Value& format = method_call.arguments()[0]; | |
| if (strcmp(format.GetString(), kTextPlainFormat) != 0) { | |
| result->Error(kClipboardError, kUnknownClipboardFormatMessage); | |
| return; | |
| } | |
| GetHasStrings(std::move(result)); | |
| } else if (method.compare(kSetClipboardDataMethod) == 0) { | |
| const rapidjson::Value& document = *method_call.arguments(); | |
| rapidjson::Value::ConstMemberIterator itr = document.FindMember(kTextKey); | |
| if (itr == document.MemberEnd()) { | |
| result->Error(kClipboardError, kUnknownClipboardFormatMessage); | |
| return; | |
| } | |
| if (!itr->value.IsString()) { | |
| result->Error(kClipboardError, kUnknownClipboardFormatMessage); | |
| return; | |
| } | |
| SetPlainText(itr->value.GetString(), std::move(result)); | |
| } else if (method.compare(kPlaySoundMethod) == 0) { | |
| // Only one string argument is expected. | |
| const rapidjson::Value& sound_type = method_call.arguments()[0]; | |
| SystemSoundPlay(sound_type.GetString(), std::move(result)); | |
| } else if (method.compare(kInitializationCompleteMethod) == 0) { | |
| // Deprecated but should not cause an error. | |
| result->Success(); | |
| } else { | |
| result->NotImplemented(); | |
| } | |
| } | |
| } // namespace flutter |