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 | |
5 | #include "flutter/shell/platform/linux/fl_platform_plugin.h" |
6 | |
7 | #include <gtk/gtk.h> |
8 | #include <cstring> |
9 | |
10 | #include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h" |
11 | #include "flutter/shell/platform/linux/public/flutter_linux/fl_method_channel.h" |
12 | |
13 | static constexpr char kChannelName[] = "flutter/platform"; |
14 | static constexpr char kBadArgumentsError[] = "Bad Arguments"; |
15 | static constexpr char kUnknownClipboardFormatError[] = |
16 | "Unknown Clipboard Format"; |
17 | static constexpr char kInProgressError[] = "In Progress"; |
18 | static constexpr char kGetClipboardDataMethod[] = "Clipboard.getData"; |
19 | static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData"; |
20 | static constexpr char kClipboardHasStringsMethod[] = "Clipboard.hasStrings"; |
21 | static constexpr char kExitApplicationMethod[] = "System.exitApplication"; |
22 | static constexpr char kRequestAppExitMethod[] = "System.requestAppExit"; |
23 | static constexpr char kInitializationCompleteMethod[] = |
24 | "System.initializationComplete"; |
25 | static constexpr char kPlaySoundMethod[] = "SystemSound.play"; |
26 | static constexpr char kSystemNavigatorPopMethod[] = "SystemNavigator.pop"; |
27 | static constexpr char kTextKey[] = "text"; |
28 | static constexpr char kValueKey[] = "value"; |
29 | |
30 | static constexpr char kExitTypeKey[] = "type"; |
31 | static constexpr char kExitTypeCancelable[] = "cancelable"; |
32 | static constexpr char kExitTypeRequired[] = "required"; |
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 | |
40 | static constexpr char kSoundTypeAlert[] = "SystemSoundType.alert"; |
41 | static constexpr char kSoundTypeClick[] = "SystemSoundType.click"; |
42 | |
43 | struct _FlPlatformPlugin { |
44 | GObject parent_instance; |
45 | |
46 | FlMethodChannel* channel; |
47 | FlMethodCall* exit_application_method_call; |
48 | GCancellable* cancellable; |
49 | bool app_initialization_complete; |
50 | }; |
51 | |
52 | G_DEFINE_TYPE(FlPlatformPlugin, fl_platform_plugin, G_TYPE_OBJECT) |
53 | |
54 | // Sends the method call response to Flutter. |
55 | static void send_response(FlMethodCall* method_call, |
56 | FlMethodResponse* response) { |
57 | g_autoptr(GError) error = nullptr; |
58 | if (!fl_method_call_respond(method_call, response, &error)) { |
59 | g_warning("Failed to send method call response: %s", error->message); |
60 | } |
61 | } |
62 | |
63 | // Called when clipboard text received. |
64 | static void clipboard_text_cb(GtkClipboard* clipboard, |
65 | const gchar* text, |
66 | gpointer user_data) { |
67 | g_autoptr(FlMethodCall) method_call = FL_METHOD_CALL(user_data); |
68 | |
69 | g_autoptr(FlValue) result = nullptr; |
70 | if (text != nullptr) { |
71 | result = fl_value_new_map(); |
72 | fl_value_set_string_take(result, kTextKey, fl_value_new_string(text)); |
73 | } |
74 | |
75 | g_autoptr(FlMethodResponse) response = |
76 | FL_METHOD_RESPONSE(fl_method_success_response_new(result)); |
77 | send_response(method_call, response); |
78 | } |
79 | |
80 | // Called when clipboard text received during has_strings. |
81 | static void clipboard_text_has_strings_cb(GtkClipboard* clipboard, |
82 | const gchar* text, |
83 | gpointer user_data) { |
84 | g_autoptr(FlMethodCall) method_call = FL_METHOD_CALL(user_data); |
85 | |
86 | g_autoptr(FlValue) result = fl_value_new_map(); |
87 | fl_value_set_string_take( |
88 | result, kValueKey, |
89 | fl_value_new_bool(text != nullptr && strlen(text) > 0)); |
90 | |
91 | g_autoptr(FlMethodResponse) response = |
92 | FL_METHOD_RESPONSE(fl_method_success_response_new(result)); |
93 | send_response(method_call, response); |
94 | } |
95 | |
96 | // Called when Flutter wants to copy to the clipboard. |
97 | static FlMethodResponse* clipboard_set_data(FlPlatformPlugin* self, |
98 | FlValue* args) { |
99 | if (fl_value_get_type(value: args) != FL_VALUE_TYPE_MAP) { |
100 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
101 | kBadArgumentsError, "Argument map missing or malformed", nullptr)); |
102 | } |
103 | |
104 | FlValue* text_value = fl_value_lookup_string(args, kTextKey); |
105 | if (text_value == nullptr || |
106 | fl_value_get_type(value: text_value) != FL_VALUE_TYPE_STRING) { |
107 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
108 | kBadArgumentsError, "Missing clipboard text", nullptr)); |
109 | } |
110 | |
111 | GtkClipboard* clipboard = |
112 | gtk_clipboard_get_default(gdk_display_get_default()); |
113 | gtk_clipboard_set_text(clipboard, fl_value_get_string(text_value), -1); |
114 | |
115 | return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); |
116 | } |
117 | |
118 | // Called when Flutter wants to paste from the clipboard. |
119 | static FlMethodResponse* clipboard_get_data_async(FlPlatformPlugin* self, |
120 | FlMethodCall* method_call) { |
121 | FlValue* args = fl_method_call_get_args(method_call); |
122 | |
123 | if (fl_value_get_type(value: args) != FL_VALUE_TYPE_STRING) { |
124 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
125 | kBadArgumentsError, "Expected string", nullptr)); |
126 | } |
127 | |
128 | const gchar* format = fl_value_get_string(args); |
129 | if (strcmp(format, kTextPlainFormat) != 0) { |
130 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
131 | kUnknownClipboardFormatError, "GTK clipboard API only supports text", |
132 | nullptr)); |
133 | } |
134 | |
135 | GtkClipboard* clipboard = |
136 | gtk_clipboard_get_default(gdk_display_get_default()); |
137 | gtk_clipboard_request_text(clipboard, clipboard_text_cb, |
138 | g_object_ref(method_call)); |
139 | |
140 | // Will respond later. |
141 | return nullptr; |
142 | } |
143 | |
144 | // Called when Flutter wants to know if the content of the clipboard is able to |
145 | // be pasted, without actually accessing the clipboard content itself. |
146 | static FlMethodResponse* clipboard_has_strings_async( |
147 | FlPlatformPlugin* self, |
148 | FlMethodCall* method_call) { |
149 | GtkClipboard* clipboard = |
150 | gtk_clipboard_get_default(gdk_display_get_default()); |
151 | gtk_clipboard_request_text(clipboard, clipboard_text_has_strings_cb, |
152 | g_object_ref(method_call)); |
153 | |
154 | // Will respond later. |
155 | return nullptr; |
156 | } |
157 | |
158 | // Get the exit response from a System.requestAppExit method call. |
159 | static gchar* get_exit_response(FlMethodResponse* response) { |
160 | if (response == nullptr) { |
161 | return nullptr; |
162 | } |
163 | |
164 | g_autoptr(GError) error = nullptr; |
165 | FlValue* result = fl_method_response_get_result(response, &error); |
166 | if (result == nullptr) { |
167 | g_warning("Error returned from System.requestAppExit: %s", error->message); |
168 | return nullptr; |
169 | } |
170 | if (fl_value_get_type(value: result) != FL_VALUE_TYPE_MAP) { |
171 | g_warning("System.requestAppExit result argument map missing or malformed"); |
172 | return nullptr; |
173 | } |
174 | |
175 | FlValue* response_value = fl_value_lookup_string(result, kExitResponseKey); |
176 | if (fl_value_get_type(value: response_value) != FL_VALUE_TYPE_STRING) { |
177 | g_warning("Invalid response from System.requestAppExit"); |
178 | return nullptr; |
179 | } |
180 | return g_strdup(fl_value_get_string(response_value)); |
181 | } |
182 | |
183 | // Quit this application |
184 | static void quit_application() { |
185 | GApplication* app = g_application_get_default(); |
186 | if (app == nullptr) { |
187 | // Unable to gracefully quit, so just exit the process. |
188 | exit(0); |
189 | } else { |
190 | g_application_quit(app); |
191 | } |
192 | } |
193 | |
194 | // Handle response of System.requestAppExit. |
195 | static void request_app_exit_response_cb(GObject* object, |
196 | GAsyncResult* result, |
197 | gpointer user_data) { |
198 | FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(user_data); |
199 | |
200 | g_autoptr(GError) error = nullptr; |
201 | g_autoptr(FlMethodResponse) method_response = |
202 | fl_method_channel_invoke_method_finish(FL_METHOD_CHANNEL(object), result, |
203 | &error); |
204 | g_autofree gchar* exit_response = nullptr; |
205 | if (method_response == nullptr) { |
206 | if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { |
207 | return; |
208 | } |
209 | g_warning("Failed to complete System.requestAppExit: %s", error->message); |
210 | } else { |
211 | exit_response = get_exit_response(method_response); |
212 | } |
213 | // If something went wrong, then just exit. |
214 | if (exit_response == nullptr) { |
215 | exit_response = g_strdup(kExitResponseExit); |
216 | } |
217 | |
218 | if (g_str_equal(exit_response, kExitResponseExit)) { |
219 | quit_application(); |
220 | } else if (g_str_equal(exit_response, kExitResponseCancel)) { |
221 | // Canceled - no action to take. |
222 | } |
223 | |
224 | // If request was due to a request from Flutter, pass result back. |
225 | if (self->exit_application_method_call != nullptr) { |
226 | g_autoptr(FlValue) exit_result = fl_value_new_map(); |
227 | fl_value_set_string_take(exit_result, kExitResponseKey, |
228 | fl_value_new_string(exit_response)); |
229 | g_autoptr(FlMethodResponse) exit_response = |
230 | FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result)); |
231 | if (!fl_method_call_respond(self->exit_application_method_call, |
232 | exit_response, &error)) { |
233 | g_warning("Failed to send response to System.exitApplication: %s", |
234 | error->message); |
235 | } |
236 | g_clear_object(&self->exit_application_method_call); |
237 | } |
238 | } |
239 | |
240 | // Send a request to Flutter to exit the application, but only if it's ready for |
241 | // a request. |
242 | static void request_app_exit(FlPlatformPlugin* self, const char* type) { |
243 | g_autoptr(FlValue) args = fl_value_new_map(); |
244 | if (!self->app_initialization_complete || |
245 | g_str_equal(type, kExitTypeRequired)) { |
246 | quit_application(); |
247 | return; |
248 | } |
249 | |
250 | fl_value_set_string_take(args, kExitTypeKey, fl_value_new_string(type)); |
251 | fl_method_channel_invoke_method(self->channel, kRequestAppExitMethod, args, |
252 | self->cancellable, |
253 | request_app_exit_response_cb, self); |
254 | } |
255 | |
256 | // Called when the Dart app has finished initialization and is ready to handle |
257 | // requests. For the Flutter framework, this means after the ServicesBinding has |
258 | // been initialized and it sends a System.initializationComplete message. |
259 | static FlMethodResponse* system_intitialization_complete( |
260 | FlPlatformPlugin* self, |
261 | FlMethodCall* method_call) { |
262 | self->app_initialization_complete = TRUE; |
263 | return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); |
264 | } |
265 | |
266 | // Called when Flutter wants to exit the application. |
267 | static FlMethodResponse* system_exit_application(FlPlatformPlugin* self, |
268 | FlMethodCall* method_call) { |
269 | FlValue* args = fl_method_call_get_args(method_call); |
270 | if (fl_value_get_type(value: args) != FL_VALUE_TYPE_MAP) { |
271 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
272 | kBadArgumentsError, "Argument map missing or malformed", nullptr)); |
273 | } |
274 | |
275 | FlValue* type_value = fl_value_lookup_string(args, kExitTypeKey); |
276 | if (type_value == nullptr || |
277 | fl_value_get_type(value: type_value) != FL_VALUE_TYPE_STRING) { |
278 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
279 | kBadArgumentsError, "Missing type argument", nullptr)); |
280 | } |
281 | const char* type = fl_value_get_string(type_value); |
282 | |
283 | // Save method call to respond to when our request to Flutter completes. |
284 | if (self->exit_application_method_call != nullptr) { |
285 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
286 | kInProgressError, "Request already in progress", nullptr)); |
287 | } |
288 | self->exit_application_method_call = |
289 | FL_METHOD_CALL(g_object_ref(method_call)); |
290 | |
291 | // Requested to immediately quit if the app hasn't yet signaled that it is |
292 | // ready to handle requests, or if the type of exit requested is "required". |
293 | if (!self->app_initialization_complete || |
294 | g_str_equal(type, kExitTypeRequired)) { |
295 | quit_application(); |
296 | g_autoptr(FlValue) exit_result = fl_value_new_map(); |
297 | fl_value_set_string_take(exit_result, kExitResponseKey, |
298 | fl_value_new_string(kExitResponseExit)); |
299 | return FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result)); |
300 | } |
301 | |
302 | // Send the request back to Flutter to follow the standard process. |
303 | request_app_exit(self, type); |
304 | |
305 | // Will respond later. |
306 | return nullptr; |
307 | } |
308 | |
309 | // Called when Flutter wants to play a sound. |
310 | static FlMethodResponse* system_sound_play(FlPlatformPlugin* self, |
311 | FlValue* args) { |
312 | if (fl_value_get_type(value: args) != FL_VALUE_TYPE_STRING) { |
313 | return FL_METHOD_RESPONSE(fl_method_error_response_new( |
314 | kBadArgumentsError, "Expected string", nullptr)); |
315 | } |
316 | |
317 | const gchar* type = fl_value_get_string(args); |
318 | if (strcmp(type, kSoundTypeAlert) == 0) { |
319 | GdkDisplay* display = gdk_display_get_default(); |
320 | if (display != nullptr) { |
321 | gdk_display_beep(display); |
322 | } |
323 | } else if (strcmp(type, kSoundTypeClick) == 0) { |
324 | // We don't make sounds for keyboard on desktops. |
325 | } else { |
326 | g_warning("Ignoring unknown sound type %s in SystemSound.play.\n", type); |
327 | } |
328 | |
329 | return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); |
330 | } |
331 | |
332 | // Called when Flutter wants to quit the application. |
333 | static FlMethodResponse* system_navigator_pop(FlPlatformPlugin* self) { |
334 | quit_application(); |
335 | return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); |
336 | } |
337 | |
338 | // Called when a method call is received from Flutter. |
339 | static void method_call_cb(FlMethodChannel* channel, |
340 | FlMethodCall* method_call, |
341 | gpointer user_data) { |
342 | FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(user_data); |
343 | |
344 | const gchar* method = fl_method_call_get_name(method_call); |
345 | FlValue* args = fl_method_call_get_args(method_call); |
346 | |
347 | g_autoptr(FlMethodResponse) response = nullptr; |
348 | if (strcmp(method, kSetClipboardDataMethod) == 0) { |
349 | response = clipboard_set_data(self, args); |
350 | } else if (strcmp(method, kGetClipboardDataMethod) == 0) { |
351 | response = clipboard_get_data_async(self, method_call); |
352 | } else if (strcmp(method, kClipboardHasStringsMethod) == 0) { |
353 | response = clipboard_has_strings_async(self, method_call); |
354 | } else if (strcmp(method, kExitApplicationMethod) == 0) { |
355 | response = system_exit_application(self, method_call); |
356 | } else if (strcmp(method, kInitializationCompleteMethod) == 0) { |
357 | response = system_intitialization_complete(self, method_call); |
358 | } else if (strcmp(method, kPlaySoundMethod) == 0) { |
359 | response = system_sound_play(self, args); |
360 | } else if (strcmp(method, kSystemNavigatorPopMethod) == 0) { |
361 | response = system_navigator_pop(self); |
362 | } else { |
363 | response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); |
364 | } |
365 | |
366 | if (response != nullptr) { |
367 | send_response(method_call, response); |
368 | } |
369 | } |
370 | |
371 | static void fl_platform_plugin_dispose(GObject* object) { |
372 | FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(object); |
373 | |
374 | g_cancellable_cancel(self->cancellable); |
375 | |
376 | g_clear_object(&self->channel); |
377 | g_clear_object(&self->exit_application_method_call); |
378 | g_clear_object(&self->cancellable); |
379 | |
380 | G_OBJECT_CLASS(fl_platform_plugin_parent_class)->dispose(object); |
381 | } |
382 | |
383 | static void fl_platform_plugin_class_init(FlPlatformPluginClass* klass) { |
384 | G_OBJECT_CLASS(klass)->dispose = fl_platform_plugin_dispose; |
385 | } |
386 | |
387 | static void fl_platform_plugin_init(FlPlatformPlugin* self) { |
388 | self->cancellable = g_cancellable_new(); |
389 | } |
390 | |
391 | FlPlatformPlugin* fl_platform_plugin_new(FlBinaryMessenger* messenger) { |
392 | g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); |
393 | |
394 | FlPlatformPlugin* self = |
395 | FL_PLATFORM_PLUGIN(g_object_new(fl_platform_plugin_get_type(), nullptr)); |
396 | |
397 | g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); |
398 | self->channel = |
399 | fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); |
400 | fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self, |
401 | nullptr); |
402 | self->app_initialization_complete = FALSE; |
403 | |
404 | return self; |
405 | } |
406 | |
407 | void fl_platform_plugin_request_app_exit(FlPlatformPlugin* self) { |
408 | g_return_if_fail(FL_IS_PLATFORM_PLUGIN(self)); |
409 | // Request a cancellable exit. |
410 | request_app_exit(self, kExitTypeCancelable); |
411 | } |
412 |
Definitions
- kChannelName
- kBadArgumentsError
- kUnknownClipboardFormatError
- kInProgressError
- kGetClipboardDataMethod
- kSetClipboardDataMethod
- kClipboardHasStringsMethod
- kExitApplicationMethod
- kRequestAppExitMethod
- kInitializationCompleteMethod
- kPlaySoundMethod
- kSystemNavigatorPopMethod
- kTextKey
- kValueKey
- kExitTypeKey
- kExitTypeCancelable
- kExitTypeRequired
- kExitResponseKey
- kExitResponseCancel
- kExitResponseExit
- kTextPlainFormat
- kSoundTypeAlert
- kSoundTypeClick
- _FlPlatformPlugin
- send_response
- clipboard_text_cb
- clipboard_text_has_strings_cb
- clipboard_set_data
- clipboard_get_data_async
- clipboard_has_strings_async
- get_exit_response
- quit_application
- request_app_exit_response_cb
- request_app_exit
- system_intitialization_complete
- system_exit_application
- system_sound_play
- system_navigator_pop
- method_call_cb
- fl_platform_plugin_dispose
- fl_platform_plugin_class_init
- fl_platform_plugin_init
- fl_platform_plugin_new
Learn more about Flutter for embedded and desktop on industrialflutter.com