Flutter Windows Embedder
platform_handler.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <windows.h>
8 
9 #include <cstring>
10 #include <optional>
11 
12 #include "flutter/fml/logging.h"
13 #include "flutter/fml/macros.h"
14 #include "flutter/fml/platform/win/wstring_conversion.h"
18 
19 static constexpr char kChannelName[] = "flutter/platform";
20 
21 static constexpr char kGetClipboardDataMethod[] = "Clipboard.getData";
22 static constexpr char kHasStringsClipboardMethod[] = "Clipboard.hasStrings";
23 static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData";
24 static constexpr char kExitApplicationMethod[] = "System.exitApplication";
25 static constexpr char kRequestAppExitMethod[] = "System.requestAppExit";
26 static constexpr char kInitializationCompleteMethod[] =
27  "System.initializationComplete";
28 static constexpr char kPlaySoundMethod[] = "SystemSound.play";
29 
30 static constexpr char kExitCodeKey[] = "exitCode";
31 
32 static constexpr char kExitTypeKey[] = "type";
33 
34 static constexpr char kExitResponseKey[] = "response";
35 static constexpr char kExitResponseCancel[] = "cancel";
36 static constexpr char kExitResponseExit[] = "exit";
37 
38 static constexpr char kTextPlainFormat[] = "text/plain";
39 static constexpr char kTextKey[] = "text";
40 static constexpr char kUnknownClipboardFormatMessage[] =
41  "Unknown clipboard format";
42 
43 static constexpr char kValueKey[] = "value";
44 static constexpr int kAccessDeniedErrorCode = 5;
45 static constexpr int kErrorSuccess = 0;
46 
47 static constexpr char kExitRequestError[] = "ExitApplication error";
48 static constexpr char kInvalidExitRequestMessage[] =
49  "Invalid application exit request";
50 
51 namespace flutter {
52 
53 namespace {
54 
55 static const std::wstring kWindowClassName = L"FlutterPlatformHandler";
56 
57 // A scoped wrapper for GlobalAlloc/GlobalFree.
58 class ScopedGlobalMemory {
59  public:
60  // Allocates |bytes| bytes of global memory with the given flags.
61  ScopedGlobalMemory(unsigned int flags, size_t bytes) {
62  memory_ = ::GlobalAlloc(flags, bytes);
63  if (!memory_) {
64  FML_LOG(ERROR) << "Unable to allocate global memory: "
65  << static_cast<int>(::GetLastError());
66  }
67  }
68 
69  ~ScopedGlobalMemory() {
70  if (memory_) {
71  if (::GlobalFree(memory_) != nullptr) {
72  FML_LOG(ERROR) << "Failed to free global allocation: "
73  << static_cast<int>(::GetLastError());
74  }
75  }
76  }
77 
78  // Returns the memory pointer, which will be nullptr if allocation failed.
79  void* get() { return memory_; }
80 
81  void* release() {
82  void* memory = memory_;
83  memory_ = nullptr;
84  return memory;
85  }
86 
87  private:
88  HGLOBAL memory_;
89 
90  FML_DISALLOW_COPY_AND_ASSIGN(ScopedGlobalMemory);
91 };
92 
93 // A scoped wrapper for GlobalLock/GlobalUnlock.
94 class ScopedGlobalLock {
95  public:
96  // Attempts to acquire a global lock on |memory| for the life of this object.
97  ScopedGlobalLock(HGLOBAL memory) {
98  source_ = memory;
99  if (memory) {
100  locked_memory_ = ::GlobalLock(memory);
101  if (!locked_memory_) {
102  FML_LOG(ERROR) << "Unable to acquire global lock: " << ::GetLastError();
103  }
104  }
105  }
106 
107  ~ScopedGlobalLock() {
108  if (locked_memory_) {
109  if (!::GlobalUnlock(source_)) {
110  DWORD error = ::GetLastError();
111  if (error != NO_ERROR) {
112  FML_LOG(ERROR) << "Unable to release global lock: "
113  << ::GetLastError();
114  }
115  }
116  }
117  }
118 
119  // Returns the locked memory pointer, which will be nullptr if acquiring the
120  // lock failed.
121  void* get() { return locked_memory_; }
122 
123  private:
124  HGLOBAL source_;
125  void* locked_memory_;
126 
127  FML_DISALLOW_COPY_AND_ASSIGN(ScopedGlobalLock);
128 };
129 
130 // A Clipboard wrapper that automatically closes the clipboard when it goes out
131 // of scope.
132 class ScopedClipboard : public ScopedClipboardInterface {
133  public:
134  ScopedClipboard();
135  virtual ~ScopedClipboard();
136 
137  int Open(HWND window) override;
138 
139  bool HasString() override;
140 
141  std::variant<std::wstring, int> GetString() override;
142 
143  int SetString(const std::wstring string) override;
144 
145  private:
146  bool opened_ = false;
147 
148  FML_DISALLOW_COPY_AND_ASSIGN(ScopedClipboard);
149 };
150 
151 ScopedClipboard::ScopedClipboard() {}
152 
153 ScopedClipboard::~ScopedClipboard() {
154  if (opened_) {
155  ::CloseClipboard();
156  }
157 }
158 
159 int ScopedClipboard::Open(HWND window) {
160  opened_ = ::OpenClipboard(window);
161 
162  if (!opened_) {
163  return ::GetLastError();
164  }
165 
166  return kErrorSuccess;
167 }
168 
169 bool ScopedClipboard::HasString() {
170  // Allow either plain text format, since getting data will auto-interpolate.
171  return ::IsClipboardFormatAvailable(CF_UNICODETEXT) ||
172  ::IsClipboardFormatAvailable(CF_TEXT);
173 }
174 
175 std::variant<std::wstring, int> ScopedClipboard::GetString() {
176  FML_DCHECK(opened_) << "Called GetString when clipboard is closed";
177 
178  HANDLE data = ::GetClipboardData(CF_UNICODETEXT);
179  if (data == nullptr) {
180  return static_cast<int>(::GetLastError());
181  }
182  ScopedGlobalLock locked_data(data);
183 
184  if (!locked_data.get()) {
185  return static_cast<int>(::GetLastError());
186  }
187  return static_cast<wchar_t*>(locked_data.get());
188 }
189 
190 int ScopedClipboard::SetString(const std::wstring string) {
191  FML_DCHECK(opened_) << "Called GetString when clipboard is closed";
192  if (!::EmptyClipboard()) {
193  return ::GetLastError();
194  }
195  size_t null_terminated_byte_count =
196  sizeof(decltype(string)::traits_type::char_type) * (string.size() + 1);
197  ScopedGlobalMemory destination_memory(GMEM_MOVEABLE,
198  null_terminated_byte_count);
199  ScopedGlobalLock locked_memory(destination_memory.get());
200  if (!locked_memory.get()) {
201  return ::GetLastError();
202  }
203  memcpy(locked_memory.get(), string.c_str(), null_terminated_byte_count);
204  if (!::SetClipboardData(CF_UNICODETEXT, locked_memory.get())) {
205  return ::GetLastError();
206  }
207  // The clipboard now owns the global memory.
208  destination_memory.release();
209  return kErrorSuccess;
210 }
211 
212 } // namespace
213 
214 static AppExitType StringToAppExitType(const std::string& string) {
215  if (string.compare(PlatformHandler::kExitTypeRequired) == 0) {
216  return AppExitType::required;
217  } else if (string.compare(PlatformHandler::kExitTypeCancelable) == 0) {
219  }
220  FML_LOG(ERROR) << string << " is not recognized as a valid exit type.";
221  return AppExitType::required;
222 }
223 
225  BinaryMessenger* messenger,
226  FlutterWindowsEngine* engine,
227  std::optional<std::function<std::unique_ptr<ScopedClipboardInterface>()>>
228  scoped_clipboard_provider)
229  : channel_(std::make_unique<MethodChannel<rapidjson::Document>>(
230  messenger,
231  kChannelName,
232  &JsonMethodCodec::GetInstance())),
233  engine_(engine) {
234  channel_->SetMethodCallHandler(
235  [this](const MethodCall<rapidjson::Document>& call,
236  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
237  HandleMethodCall(call, std::move(result));
238  });
239  if (scoped_clipboard_provider.has_value()) {
240  scoped_clipboard_provider_ = scoped_clipboard_provider.value();
241  } else {
242  scoped_clipboard_provider_ = []() {
243  return std::make_unique<ScopedClipboard>();
244  };
245  }
246 
247  WNDCLASS window_class = RegisterWindowClass();
248  window_handle_ =
249  CreateWindowEx(0, window_class.lpszClassName, L"", 0, 0, 0, 0, 0,
250  HWND_MESSAGE, nullptr, window_class.hInstance, nullptr);
251 
252  if (window_handle_) {
253  SetWindowLongPtr(window_handle_, GWLP_USERDATA,
254  reinterpret_cast<LONG_PTR>(this));
255  } else {
256  auto error = GetLastError();
257  LPWSTR message = nullptr;
258  size_t size = FormatMessageW(
259  FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
260  FORMAT_MESSAGE_IGNORE_INSERTS,
261  NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
262  reinterpret_cast<LPWSTR>(&message), 0, NULL);
263  OutputDebugString(message);
264  LocalFree(message);
265  }
266 }
267 
269  if (window_handle_) {
270  DestroyWindow(window_handle_);
271  window_handle_ = nullptr;
272  }
273  UnregisterClass(kWindowClassName.c_str(), nullptr);
274 }
275 
277  std::unique_ptr<MethodResult<rapidjson::Document>> result,
278  std::string_view key) {
279  std::unique_ptr<ScopedClipboardInterface> clipboard =
280  scoped_clipboard_provider_();
281 
282  int open_result = clipboard->Open(window_handle_);
283  if (open_result != kErrorSuccess) {
284  rapidjson::Document error_code;
285  error_code.SetInt(open_result);
286  result->Error(kClipboardError, "Unable to open clipboard", error_code);
287  return;
288  }
289  if (!clipboard->HasString()) {
290  result->Success(rapidjson::Document());
291  return;
292  }
293  std::variant<std::wstring, int> get_string_result = clipboard->GetString();
294  if (std::holds_alternative<int>(get_string_result)) {
295  rapidjson::Document error_code;
296  error_code.SetInt(std::get<int>(get_string_result));
297  result->Error(kClipboardError, "Unable to get clipboard data", error_code);
298  return;
299  }
300 
301  rapidjson::Document document;
302  document.SetObject();
303  rapidjson::Document::AllocatorType& allocator = document.GetAllocator();
304  document.AddMember(
305  rapidjson::Value(key.data(), allocator),
306  rapidjson::Value(
307  fml::WideStringToUtf8(std::get<std::wstring>(get_string_result)),
308  allocator),
309  allocator);
310  result->Success(document);
311 }
312 
314  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
315  std::unique_ptr<ScopedClipboardInterface> clipboard =
316  scoped_clipboard_provider_();
317 
318  bool hasStrings;
319  int open_result = clipboard->Open(window_handle_);
320  if (open_result != kErrorSuccess) {
321  // Swallow errors of type ERROR_ACCESS_DENIED. These happen when the app is
322  // not in the foreground and GetHasStrings is irrelevant.
323  // See https://github.com/flutter/flutter/issues/95817.
324  if (open_result != kAccessDeniedErrorCode) {
325  rapidjson::Document error_code;
326  error_code.SetInt(open_result);
327  result->Error(kClipboardError, "Unable to open clipboard", error_code);
328  return;
329  }
330  hasStrings = false;
331  } else {
332  hasStrings = clipboard->HasString();
333  }
334 
335  rapidjson::Document document;
336  document.SetObject();
337  rapidjson::Document::AllocatorType& allocator = document.GetAllocator();
338  document.AddMember(rapidjson::Value(kValueKey, allocator),
339  rapidjson::Value(hasStrings), allocator);
340  result->Success(document);
341 }
342 
344  const std::string& text,
345  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
346  std::unique_ptr<ScopedClipboardInterface> clipboard =
347  scoped_clipboard_provider_();
348 
349  int open_result = clipboard->Open(window_handle_);
350  if (open_result != kErrorSuccess) {
351  rapidjson::Document error_code;
352  error_code.SetInt(open_result);
353  result->Error(kClipboardError, "Unable to open clipboard", error_code);
354  return;
355  }
356  int set_result = clipboard->SetString(fml::Utf8ToWideString(text));
357  if (set_result != kErrorSuccess) {
358  rapidjson::Document error_code;
359  error_code.SetInt(set_result);
360  result->Error(kClipboardError, "Unable to set clipboard data", error_code);
361  return;
362  }
363  result->Success();
364 }
365 
367  const std::string& sound_type,
368  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
369  if (sound_type.compare(kSoundTypeAlert) == 0) {
370  MessageBeep(MB_OK);
371  result->Success();
372  } else if (sound_type.compare(kSoundTypeClick) == 0) {
373  // No-op, as there is no system sound for key presses.
374  result->Success();
375  } else if (sound_type.compare(kSoundTypeTick) == 0) {
376  // No-op, as there is no system sound for ticks.
377  result->Success();
378  } else {
379  result->NotImplemented();
380  }
381 }
382 
384  AppExitType exit_type,
385  UINT exit_code,
386  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
387  rapidjson::Document result_doc;
388  result_doc.SetObject();
389  if (exit_type == AppExitType::required) {
390  QuitApplication(std::nullopt, std::nullopt, std::nullopt, exit_code);
391  result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseExit,
392  result_doc.GetAllocator());
393  result->Success(result_doc);
394  } else {
395  RequestAppExit(std::nullopt, std::nullopt, std::nullopt, exit_type,
396  exit_code);
397  result_doc.GetObjectW().AddMember(kExitResponseKey, kExitResponseCancel,
398  result_doc.GetAllocator());
399  result->Success(result_doc);
400  }
401 }
402 
403 // Indicates whether an exit request may be canceled by the framework.
404 // These values must be kept in sync with ExitType in platform_handler.h
405 static constexpr const char* kExitTypeNames[] = {
407 
408 void PlatformHandler::RequestAppExit(std::optional<HWND> hwnd,
409  std::optional<WPARAM> wparam,
410  std::optional<LPARAM> lparam,
411  AppExitType exit_type,
412  UINT exit_code) {
413  auto callback = std::make_unique<MethodResultFunctions<rapidjson::Document>>(
414  [this, exit_code, hwnd, wparam,
415  lparam](const rapidjson::Document* response) {
416  RequestAppExitSuccess(hwnd, wparam, lparam, response, exit_code);
417  },
418  nullptr, nullptr);
419  auto args = std::make_unique<rapidjson::Document>();
420  args->SetObject();
421  args->GetObjectW().AddMember(
422  kExitTypeKey, std::string(kExitTypeNames[static_cast<int>(exit_type)]),
423  args->GetAllocator());
424  channel_->InvokeMethod(kRequestAppExitMethod, std::move(args),
425  std::move(callback));
426 }
427 
428 void PlatformHandler::RequestAppExitSuccess(std::optional<HWND> hwnd,
429  std::optional<WPARAM> wparam,
430  std::optional<LPARAM> lparam,
431  const rapidjson::Document* result,
432  UINT exit_code) {
433  rapidjson::Value::ConstMemberIterator itr =
434  result->FindMember(kExitResponseKey);
435  if (itr == result->MemberEnd() || !itr->value.IsString()) {
436  FML_LOG(ERROR) << "Application request response did not contain a valid "
437  "response value";
438  return;
439  }
440  const std::string& exit_type = itr->value.GetString();
441 
442  if (exit_type.compare(kExitResponseExit) == 0) {
443  QuitApplication(hwnd, wparam, lparam, exit_code);
444  }
445 }
446 
447 void PlatformHandler::QuitApplication(std::optional<HWND> hwnd,
448  std::optional<WPARAM> wparam,
449  std::optional<LPARAM> lparam,
450  UINT exit_code) {
451  engine_->OnQuit(hwnd, wparam, lparam, exit_code);
452 }
453 
454 void PlatformHandler::HandleMethodCall(
455  const MethodCall<rapidjson::Document>& method_call,
456  std::unique_ptr<MethodResult<rapidjson::Document>> result) {
457  const std::string& method = method_call.method_name();
458  if (method.compare(kExitApplicationMethod) == 0) {
459  const rapidjson::Value& arguments = method_call.arguments()[0];
460 
461  rapidjson::Value::ConstMemberIterator itr =
462  arguments.FindMember(kExitTypeKey);
463  if (itr == arguments.MemberEnd() || !itr->value.IsString()) {
465  return;
466  }
467  const std::string& exit_type = itr->value.GetString();
468 
469  itr = arguments.FindMember(kExitCodeKey);
470  if (itr == arguments.MemberEnd() || !itr->value.IsInt()) {
472  return;
473  }
474  UINT exit_code = arguments[kExitCodeKey].GetInt();
475 
476  SystemExitApplication(StringToAppExitType(exit_type), exit_code,
477  std::move(result));
478  } else if (method.compare(kGetClipboardDataMethod) == 0) {
479  // Only one string argument is expected.
480  const rapidjson::Value& format = method_call.arguments()[0];
481 
482  if (strcmp(format.GetString(), kTextPlainFormat) != 0) {
484  return;
485  }
486  GetPlainText(std::move(result), kTextKey);
487  } else if (method.compare(kHasStringsClipboardMethod) == 0) {
488  // Only one string argument is expected.
489  const rapidjson::Value& format = method_call.arguments()[0];
490 
491  if (strcmp(format.GetString(), kTextPlainFormat) != 0) {
493  return;
494  }
495  GetHasStrings(std::move(result));
496  } else if (method.compare(kSetClipboardDataMethod) == 0) {
497  const rapidjson::Value& document = *method_call.arguments();
498  rapidjson::Value::ConstMemberIterator itr = document.FindMember(kTextKey);
499  if (itr == document.MemberEnd()) {
501  return;
502  }
503  if (!itr->value.IsString()) {
505  return;
506  }
507  SetPlainText(itr->value.GetString(), std::move(result));
508  } else if (method.compare(kPlaySoundMethod) == 0) {
509  // Only one string argument is expected.
510  const rapidjson::Value& sound_type = method_call.arguments()[0];
511 
512  SystemSoundPlay(sound_type.GetString(), std::move(result));
513  } else if (method.compare(kInitializationCompleteMethod) == 0) {
514  // Deprecated but should not cause an error.
515  result->Success();
516  } else {
517  result->NotImplemented();
518  }
519 }
520 
521 WNDCLASS PlatformHandler::RegisterWindowClass() {
522  WNDCLASS window_class{};
523  window_class.hCursor = nullptr;
524  window_class.lpszClassName = kWindowClassName.c_str();
525  window_class.style = 0;
526  window_class.cbClsExtra = 0;
527  window_class.cbWndExtra = 0;
528  window_class.hInstance = GetModuleHandle(nullptr);
529  window_class.hIcon = nullptr;
530  window_class.hbrBackground = 0;
531  window_class.lpszMenuName = nullptr;
532  window_class.lpfnWndProc = WndProc;
533  RegisterClass(&window_class);
534  return window_class;
535 }
536 
537 LRESULT PlatformHandler::WndProc(HWND const window,
538  UINT const message,
539  WPARAM const wparam,
540  LPARAM const lparam) noexcept {
541  return DefWindowProc(window, message, wparam, lparam);
542 }
543 
544 } // namespace flutter
void OnQuit(std::optional< HWND > hwnd, std::optional< WPARAM > wparam, std::optional< LPARAM > lparam, UINT exit_code)
const std::string & method_name() const
Definition: method_call.h:31
const T * arguments() const
Definition: method_call.h:34
virtual void QuitApplication(std::optional< HWND > hwnd, std::optional< WPARAM > wparam, std::optional< LPARAM > lparam, UINT exit_code)
virtual void GetHasStrings(std::unique_ptr< MethodResult< rapidjson::Document >> result)
static constexpr char kSoundTypeTick[]
virtual void RequestAppExitSuccess(std::optional< HWND > hwnd, std::optional< WPARAM > wparam, std::optional< LPARAM > lparam, const rapidjson::Document *result, UINT exit_code)
static constexpr char kClipboardError[]
PlatformHandler(BinaryMessenger *messenger, FlutterWindowsEngine *engine, std::optional< std::function< std::unique_ptr< ScopedClipboardInterface >()>> scoped_clipboard_provider=std::nullopt)
static constexpr char kSoundTypeClick[]
virtual void SystemSoundPlay(const std::string &sound_type, std::unique_ptr< MethodResult< rapidjson::Document >> result)
static constexpr char kExitTypeCancelable[]
virtual void SystemExitApplication(AppExitType exit_type, UINT exit_code, std::unique_ptr< MethodResult< rapidjson::Document >> result)
virtual void GetPlainText(std::unique_ptr< MethodResult< rapidjson::Document >> result, std::string_view key)
virtual void RequestAppExit(std::optional< HWND > hwnd, std::optional< WPARAM > wparam, std::optional< LPARAM > lparam, AppExitType exit_type, UINT exit_code)
static constexpr char kExitTypeRequired[]
static constexpr char kSoundTypeAlert[]
virtual void SetPlainText(const std::string &text, std::unique_ptr< MethodResult< rapidjson::Document >> result)
FlutterDesktopBinaryReply callback
std::u16string text
Win32Message message
static constexpr const char * kExitTypeNames[]
static AppExitType StringToAppExitType(const std::string &string)
static constexpr char kValueKey[]
static constexpr char kInitializationCompleteMethod[]
static constexpr char kInvalidExitRequestMessage[]
static constexpr char kGetClipboardDataMethod[]
static constexpr char kChannelName[]
static constexpr char kHasStringsClipboardMethod[]
static constexpr char kExitApplicationMethod[]
static constexpr char kExitResponseExit[]
static constexpr char kExitCodeKey[]
static constexpr int kErrorSuccess
static constexpr char kRequestAppExitMethod[]
static constexpr char kUnknownClipboardFormatMessage[]
static constexpr char kExitResponseKey[]
static constexpr char kPlaySoundMethod[]
static constexpr int kAccessDeniedErrorCode
static constexpr char kExitResponseCancel[]
static constexpr char kExitTypeKey[]
static constexpr char kTextPlainFormat[]
static constexpr char kTextKey[]
static constexpr char kSetClipboardDataMethod[]
static constexpr char kExitRequestError[]