blob: 197de59345351790a18a3b1751f49ce893e95677 [file] [log] [blame]
// 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