Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 62e6a65

Browse files
Add platform channel System.exitApplication and System.requestAppExit support (#40033)
Add platform channel System.exitApplication and System.requestAppExit support
1 parent e9ca7b2 commit 62e6a65

File tree

4 files changed

+270
-10
lines changed

4 files changed

+270
-10
lines changed

shell/platform/linux/fl_platform_plugin.cc

Lines changed: 161 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,25 @@ static constexpr char kChannelName[] = "flutter/platform";
1414
static constexpr char kBadArgumentsError[] = "Bad Arguments";
1515
static constexpr char kUnknownClipboardFormatError[] =
1616
"Unknown Clipboard Format";
17-
static constexpr char kFailedError[] = "Failed";
17+
static constexpr char kInProgressError[] = "In Progress";
1818
static constexpr char kGetClipboardDataMethod[] = "Clipboard.getData";
1919
static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData";
2020
static constexpr char kClipboardHasStringsMethod[] = "Clipboard.hasStrings";
21+
static constexpr char kExitApplicationMethod[] = "System.exitApplication";
22+
static constexpr char kRequestAppExitMethod[] = "System.requestAppExit";
2123
static constexpr char kPlaySoundMethod[] = "SystemSound.play";
2224
static constexpr char kSystemNavigatorPopMethod[] = "SystemNavigator.pop";
2325
static constexpr char kTextKey[] = "text";
2426
static constexpr char kValueKey[] = "value";
2527

28+
static constexpr char kExitTypeKey[] = "type";
29+
static constexpr char kExitTypeCancelable[] = "cancelable";
30+
static constexpr char kExitTypeRequired[] = "required";
31+
32+
static constexpr char kExitResponseKey[] = "response";
33+
static constexpr char kExitResponseCancel[] = "cancel";
34+
static constexpr char kExitResponseExit[] = "exit";
35+
2636
static constexpr char kTextPlainFormat[] = "text/plain";
2737

2838
static constexpr char kSoundTypeAlert[] = "SystemSoundType.alert";
@@ -32,6 +42,8 @@ struct _FlPlatformPlugin {
3242
GObject parent_instance;
3343

3444
FlMethodChannel* channel;
45+
FlMethodCall* exit_application_method_call;
46+
GCancellable* cancellable;
3547
};
3648

3749
G_DEFINE_TYPE(FlPlatformPlugin, fl_platform_plugin, G_TYPE_OBJECT)
@@ -140,6 +152,138 @@ static FlMethodResponse* clipboard_has_strings_async(
140152
return nullptr;
141153
}
142154

155+
// Get the exit response from a System.requestAppExit method call.
156+
static gchar* get_exit_response(FlMethodResponse* response) {
157+
if (response == nullptr) {
158+
return nullptr;
159+
}
160+
161+
g_autoptr(GError) error = nullptr;
162+
FlValue* result = fl_method_response_get_result(response, &error);
163+
if (result == nullptr) {
164+
g_warning("Error returned from System.requestAppExit: %s", error->message);
165+
return nullptr;
166+
}
167+
if (fl_value_get_type(result) != FL_VALUE_TYPE_MAP) {
168+
g_warning("System.requestAppExit result argument map missing or malformed");
169+
return nullptr;
170+
}
171+
172+
FlValue* response_value = fl_value_lookup_string(result, kExitResponseKey);
173+
if (fl_value_get_type(response_value) != FL_VALUE_TYPE_STRING) {
174+
g_warning("Invalid response from System.requestAppExit");
175+
return nullptr;
176+
}
177+
return g_strdup(fl_value_get_string(response_value));
178+
}
179+
180+
// Quit this application
181+
static void quit_application() {
182+
GApplication* app = g_application_get_default();
183+
if (app == nullptr) {
184+
// Unable to gracefully quit, so just exit the process.
185+
exit(0);
186+
} else {
187+
g_application_quit(app);
188+
}
189+
}
190+
191+
// Handle response of System.requestAppExit.
192+
static void request_app_exit_response_cb(GObject* object,
193+
GAsyncResult* result,
194+
gpointer user_data) {
195+
FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(user_data);
196+
197+
g_autoptr(GError) error = nullptr;
198+
g_autoptr(FlMethodResponse) method_response =
199+
fl_method_channel_invoke_method_finish(FL_METHOD_CHANNEL(object), result,
200+
&error);
201+
g_autofree gchar* exit_response = nullptr;
202+
if (method_response == nullptr) {
203+
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
204+
return;
205+
}
206+
g_warning("Failed to complete System.requestAppExit: %s", error->message);
207+
} else {
208+
exit_response = get_exit_response(method_response);
209+
}
210+
// If something went wrong, then just exit.
211+
if (exit_response == nullptr) {
212+
exit_response = g_strdup(kExitResponseExit);
213+
}
214+
215+
if (g_str_equal(exit_response, kExitResponseExit)) {
216+
quit_application();
217+
} else if (g_str_equal(exit_response, kExitResponseCancel)) {
218+
// Canceled - no action to take.
219+
}
220+
221+
// If request was due to a request from Flutter, pass result back.
222+
if (self->exit_application_method_call != nullptr) {
223+
g_autoptr(FlValue) exit_result = fl_value_new_map();
224+
fl_value_set_string_take(exit_result, kExitResponseKey,
225+
fl_value_new_string(exit_response));
226+
g_autoptr(FlMethodResponse) exit_response =
227+
FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result));
228+
if (!fl_method_call_respond(self->exit_application_method_call,
229+
exit_response, &error)) {
230+
g_warning("Failed to send response to System.exitApplication: %s",
231+
error->message);
232+
}
233+
g_clear_object(&self->exit_application_method_call);
234+
}
235+
}
236+
237+
// Send a request to Flutter to exit the application.
238+
static void request_app_exit(FlPlatformPlugin* self, const char* type) {
239+
g_autoptr(FlValue) args = fl_value_new_map();
240+
fl_value_set_string_take(args, kExitTypeKey, fl_value_new_string(type));
241+
fl_method_channel_invoke_method(self->channel, kRequestAppExitMethod, args,
242+
self->cancellable,
243+
request_app_exit_response_cb, self);
244+
}
245+
246+
// Called when Flutter wants to exit the application.
247+
static FlMethodResponse* system_exit_application(FlPlatformPlugin* self,
248+
FlMethodCall* method_call) {
249+
FlValue* args = fl_method_call_get_args(method_call);
250+
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
251+
return FL_METHOD_RESPONSE(fl_method_error_response_new(
252+
kBadArgumentsError, "Argument map missing or malformed", nullptr));
253+
}
254+
255+
FlValue* type_value = fl_value_lookup_string(args, kExitTypeKey);
256+
if (type_value == nullptr ||
257+
fl_value_get_type(type_value) != FL_VALUE_TYPE_STRING) {
258+
return FL_METHOD_RESPONSE(fl_method_error_response_new(
259+
kBadArgumentsError, "Missing type argument", nullptr));
260+
}
261+
const char* type = fl_value_get_string(type_value);
262+
263+
// Save method call to respond to when our request to Flutter completes.
264+
if (self->exit_application_method_call != nullptr) {
265+
return FL_METHOD_RESPONSE(fl_method_error_response_new(
266+
kInProgressError, "Request already in progress", nullptr));
267+
}
268+
self->exit_application_method_call =
269+
FL_METHOD_CALL(g_object_ref(method_call));
270+
271+
// Requested to immediately quit.
272+
if (g_str_equal(type, kExitTypeRequired)) {
273+
quit_application();
274+
g_autoptr(FlValue) exit_result = fl_value_new_map();
275+
fl_value_set_string_take(exit_result, kExitResponseKey,
276+
fl_value_new_string(kExitResponseExit));
277+
return FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result));
278+
}
279+
280+
// Send the request back to Flutter to follow the standard process.
281+
request_app_exit(self, type);
282+
283+
// Will respond later.
284+
return nullptr;
285+
}
286+
143287
// Called when Flutter wants to play a sound.
144288
static FlMethodResponse* system_sound_play(FlPlatformPlugin* self,
145289
FlValue* args) {
@@ -165,14 +309,7 @@ static FlMethodResponse* system_sound_play(FlPlatformPlugin* self,
165309

166310
// Called when Flutter wants to quit the application.
167311
static FlMethodResponse* system_navigator_pop(FlPlatformPlugin* self) {
168-
GApplication* app = g_application_get_default();
169-
if (app == nullptr) {
170-
return FL_METHOD_RESPONSE(fl_method_error_response_new(
171-
kFailedError, "Unable to get GApplication", nullptr));
172-
}
173-
174-
g_application_quit(app);
175-
312+
quit_application();
176313
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
177314
}
178315

@@ -192,6 +329,8 @@ static void method_call_cb(FlMethodChannel* channel,
192329
response = clipboard_get_data_async(self, method_call);
193330
} else if (strcmp(method, kClipboardHasStringsMethod) == 0) {
194331
response = clipboard_has_strings_async(self, method_call);
332+
} else if (strcmp(method, kExitApplicationMethod) == 0) {
333+
response = system_exit_application(self, method_call);
195334
} else if (strcmp(method, kPlaySoundMethod) == 0) {
196335
response = system_sound_play(self, args);
197336
} else if (strcmp(method, kSystemNavigatorPopMethod) == 0) {
@@ -208,7 +347,11 @@ static void method_call_cb(FlMethodChannel* channel,
208347
static void fl_platform_plugin_dispose(GObject* object) {
209348
FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(object);
210349

350+
g_cancellable_cancel(self->cancellable);
351+
211352
g_clear_object(&self->channel);
353+
g_clear_object(&self->exit_application_method_call);
354+
g_clear_object(&self->cancellable);
212355

213356
G_OBJECT_CLASS(fl_platform_plugin_parent_class)->dispose(object);
214357
}
@@ -217,7 +360,9 @@ static void fl_platform_plugin_class_init(FlPlatformPluginClass* klass) {
217360
G_OBJECT_CLASS(klass)->dispose = fl_platform_plugin_dispose;
218361
}
219362

220-
static void fl_platform_plugin_init(FlPlatformPlugin* self) {}
363+
static void fl_platform_plugin_init(FlPlatformPlugin* self) {
364+
self->cancellable = g_cancellable_new();
365+
}
221366

222367
FlPlatformPlugin* fl_platform_plugin_new(FlBinaryMessenger* messenger) {
223368
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
@@ -233,3 +378,9 @@ FlPlatformPlugin* fl_platform_plugin_new(FlBinaryMessenger* messenger) {
233378

234379
return self;
235380
}
381+
382+
void fl_platform_plugin_request_app_exit(FlPlatformPlugin* self) {
383+
g_return_if_fail(FL_IS_PLATFORM_PLUGIN(self));
384+
// Request a cancellable exit.
385+
request_app_exit(self, kExitTypeCancelable);
386+
}

shell/platform/linux/fl_platform_plugin.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ G_DECLARE_FINAL_TYPE(FlPlatformPlugin,
3333
*/
3434
FlPlatformPlugin* fl_platform_plugin_new(FlBinaryMessenger* messenger);
3535

36+
/**
37+
* fl_platform_plugin_request_app_exit:
38+
* @plugin: an #FlPlatformPlugin
39+
*
40+
* Request the application exits (i.e. due to the window being requested to be
41+
* closed).
42+
*/
43+
void fl_platform_plugin_request_app_exit(FlPlatformPlugin* plugin);
44+
3645
G_END_DECLS
3746

3847
#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_PLATFORM_PLUGIN_H_

shell/platform/linux/fl_platform_plugin_test.cc

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,67 @@ MATCHER_P(SuccessResponse, result, "") {
2626
return false;
2727
}
2828

29+
class MethodCallMatcher {
30+
public:
31+
using is_gtest_matcher = void;
32+
33+
explicit MethodCallMatcher(::testing::Matcher<std::string> name,
34+
::testing::Matcher<FlValue*> args)
35+
: name_(std::move(name)), args_(std::move(args)) {}
36+
37+
bool MatchAndExplain(GBytes* method_call,
38+
::testing::MatchResultListener* result_listener) const {
39+
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
40+
g_autoptr(GError) error = nullptr;
41+
g_autofree gchar* name = nullptr;
42+
g_autoptr(FlValue) args = nullptr;
43+
gboolean result = fl_method_codec_decode_method_call(
44+
FL_METHOD_CODEC(codec), method_call, &name, &args, &error);
45+
if (!result) {
46+
*result_listener << ::testing::PrintToString(error->message);
47+
return false;
48+
}
49+
if (!name_.MatchAndExplain(name, result_listener)) {
50+
*result_listener << " where the name doesn't match: \"" << name << "\"";
51+
return false;
52+
}
53+
if (!args_.MatchAndExplain(args, result_listener)) {
54+
*result_listener << " where the args don't match: "
55+
<< ::testing::PrintToString(args);
56+
return false;
57+
}
58+
return true;
59+
}
60+
61+
void DescribeTo(std::ostream* os) const {
62+
*os << "method name ";
63+
name_.DescribeTo(os);
64+
*os << " and args ";
65+
args_.DescribeTo(os);
66+
}
67+
68+
void DescribeNegationTo(std::ostream* os) const {
69+
*os << "method name ";
70+
name_.DescribeNegationTo(os);
71+
*os << " or args ";
72+
args_.DescribeNegationTo(os);
73+
}
74+
75+
private:
76+
::testing::Matcher<std::string> name_;
77+
::testing::Matcher<FlValue*> args_;
78+
};
79+
80+
static ::testing::Matcher<GBytes*> MethodCall(
81+
const std::string& name,
82+
::testing::Matcher<FlValue*> args) {
83+
return MethodCallMatcher(::testing::StrEq(name), std::move(args));
84+
}
85+
86+
MATCHER_P(FlValueEq, value, "equal to " + ::testing::PrintToString(value)) {
87+
return fl_value_equal(arg, value);
88+
}
89+
2990
TEST(FlPlatformPluginTest, PlaySound) {
3091
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
3192

@@ -45,3 +106,28 @@ TEST(FlPlatformPluginTest, PlaySound) {
45106

46107
messenger.ReceiveMessage("flutter/platform", message);
47108
}
109+
110+
TEST(FlPlatformPluginTest, ExitApplication) {
111+
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
112+
113+
g_autoptr(FlPlatformPlugin) plugin = fl_platform_plugin_new(messenger);
114+
EXPECT_NE(plugin, nullptr);
115+
116+
g_autoptr(FlValue) args = fl_value_new_map();
117+
fl_value_set_string_take(args, "type", fl_value_new_string("cancelable"));
118+
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
119+
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
120+
FL_METHOD_CODEC(codec), "System.exitApplication", args, nullptr);
121+
122+
g_autoptr(FlValue) requestArgs = fl_value_new_map();
123+
fl_value_set_string_take(requestArgs, "type",
124+
fl_value_new_string("cancelable"));
125+
EXPECT_CALL(messenger,
126+
fl_binary_messenger_send_on_channel(
127+
::testing::Eq<FlBinaryMessenger*>(messenger),
128+
::testing::StrEq("flutter/platform"),
129+
MethodCall("System.requestAppExit", FlValueEq(requestArgs)),
130+
::testing::_, ::testing::_, ::testing::_));
131+
132+
messenger.ReceiveMessage("flutter/platform", message);
133+
}

shell/platform/linux/fl_view.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ G_DEFINE_TYPE_WITH_CODE(
8989
G_IMPLEMENT_INTERFACE(fl_text_input_view_delegate_get_type(),
9090
fl_view_text_input_delegate_iface_init))
9191

92+
// Signal handler for GtkWidget::delete-event
93+
static gboolean window_delete_event_cb(GtkWidget* widget,
94+
GdkEvent* event,
95+
FlView* self) {
96+
fl_platform_plugin_request_app_exit(self->platform_plugin);
97+
// Stop the event from propagating.
98+
return TRUE;
99+
}
100+
92101
// Initialize keyboard manager.
93102
static void init_keyboard(FlView* self) {
94103
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
@@ -475,6 +484,11 @@ static void realize_cb(GtkWidget* widget) {
475484
FlView* self = FL_VIEW(widget);
476485
g_autoptr(GError) error = nullptr;
477486

487+
// Handle requests by the user to close the application.
488+
GtkWidget* toplevel_window = gtk_widget_get_toplevel(GTK_WIDGET(self));
489+
g_signal_connect(toplevel_window, "delete-event",
490+
G_CALLBACK(window_delete_event_cb), self);
491+
478492
init_keyboard(self);
479493

480494
if (!fl_renderer_start(self->renderer, self, &error)) {

0 commit comments

Comments
 (0)