Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 161 additions & 10 deletions shell/platform/linux/fl_platform_plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@ static constexpr char kChannelName[] = "flutter/platform";
static constexpr char kBadArgumentsError[] = "Bad Arguments";
static constexpr char kUnknownClipboardFormatError[] =
"Unknown Clipboard Format";
static constexpr char kFailedError[] = "Failed";
static constexpr char kInProgressError[] = "In Progress";
static constexpr char kGetClipboardDataMethod[] = "Clipboard.getData";
static constexpr char kSetClipboardDataMethod[] = "Clipboard.setData";
static constexpr char kClipboardHasStringsMethod[] = "Clipboard.hasStrings";
static constexpr char kExitApplicationMethod[] = "System.exitApplication";
static constexpr char kRequestAppExitMethod[] = "System.requestAppExit";
static constexpr char kPlaySoundMethod[] = "SystemSound.play";
static constexpr char kSystemNavigatorPopMethod[] = "SystemNavigator.pop";
static constexpr char kTextKey[] = "text";
static constexpr char kValueKey[] = "value";

static constexpr char kExitTypeKey[] = "type";
static constexpr char kExitTypeCancelable[] = "cancelable";
static constexpr char kExitTypeRequired[] = "required";

static constexpr char kExitResponseKey[] = "response";
static constexpr char kExitResponseCancel[] = "cancel";
static constexpr char kExitResponseExit[] = "exit";

static constexpr char kTextPlainFormat[] = "text/plain";

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

FlMethodChannel* channel;
FlMethodCall* exit_application_method_call;
GCancellable* cancellable;
};

G_DEFINE_TYPE(FlPlatformPlugin, fl_platform_plugin, G_TYPE_OBJECT)
Expand Down Expand Up @@ -140,6 +152,138 @@ static FlMethodResponse* clipboard_has_strings_async(
return nullptr;
}

// Get the exit response from a System.requestAppExit method call.
static gchar* get_exit_response(FlMethodResponse* response) {
if (response == nullptr) {
return nullptr;
}

g_autoptr(GError) error = nullptr;
FlValue* result = fl_method_response_get_result(response, &error);
if (result == nullptr) {
g_warning("Error returned from System.requestAppExit: %s", error->message);
return nullptr;
}
if (fl_value_get_type(result) != FL_VALUE_TYPE_MAP) {
g_warning("System.requestAppExit result argument map missing or malformed");
return nullptr;
}

FlValue* response_value = fl_value_lookup_string(result, kExitResponseKey);
if (fl_value_get_type(response_value) != FL_VALUE_TYPE_STRING) {
g_warning("Invalid response from System.requestAppExit");
return nullptr;
}
return g_strdup(fl_value_get_string(response_value));
}

// Quit this application
static void quit_application() {
GApplication* app = g_application_get_default();
if (app == nullptr) {
// Unable to gracefully quit, so just exit the process.
exit(0);
} else {
g_application_quit(app);
}
}

// Handle response of System.requestAppExit.
static void request_app_exit_response_cb(GObject* object,
GAsyncResult* result,
gpointer user_data) {
FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(user_data);

g_autoptr(GError) error = nullptr;
g_autoptr(FlMethodResponse) method_response =
fl_method_channel_invoke_method_finish(FL_METHOD_CHANNEL(object), result,
&error);
g_autofree gchar* exit_response = nullptr;
if (method_response == nullptr) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
return;
}
g_warning("Failed to complete System.requestAppExit: %s", error->message);
} else {
exit_response = get_exit_response(method_response);
}
// If something went wrong, then just exit.
if (exit_response == nullptr) {
exit_response = g_strdup(kExitResponseExit);
}

if (g_str_equal(exit_response, kExitResponseExit)) {
quit_application();
} else if (g_str_equal(exit_response, kExitResponseCancel)) {
// Canceled - no action to take.
}

// If request was due to a request from Flutter, pass result back.
if (self->exit_application_method_call != nullptr) {
g_autoptr(FlValue) exit_result = fl_value_new_map();
fl_value_set_string_take(exit_result, kExitResponseKey,
fl_value_new_string(exit_response));
g_autoptr(FlMethodResponse) exit_response =
FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result));
if (!fl_method_call_respond(self->exit_application_method_call,
exit_response, &error)) {
g_warning("Failed to send response to System.exitApplication: %s",
error->message);
}
g_clear_object(&self->exit_application_method_call);
}
}

// Send a request to Flutter to exit the application.
static void request_app_exit(FlPlatformPlugin* self, const char* type) {
g_autoptr(FlValue) args = fl_value_new_map();
fl_value_set_string_take(args, kExitTypeKey, fl_value_new_string(type));
fl_method_channel_invoke_method(self->channel, kRequestAppExitMethod, args,
self->cancellable,
request_app_exit_response_cb, self);
}

// Called when Flutter wants to exit the application.
static FlMethodResponse* system_exit_application(FlPlatformPlugin* self,
FlMethodCall* method_call) {
FlValue* args = fl_method_call_get_args(method_call);
if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
return FL_METHOD_RESPONSE(fl_method_error_response_new(
kBadArgumentsError, "Argument map missing or malformed", nullptr));
}

FlValue* type_value = fl_value_lookup_string(args, kExitTypeKey);
if (type_value == nullptr ||
fl_value_get_type(type_value) != FL_VALUE_TYPE_STRING) {
return FL_METHOD_RESPONSE(fl_method_error_response_new(
kBadArgumentsError, "Missing type argument", nullptr));
}
const char* type = fl_value_get_string(type_value);

// Save method call to respond to when our request to Flutter completes.
if (self->exit_application_method_call != nullptr) {
return FL_METHOD_RESPONSE(fl_method_error_response_new(
kInProgressError, "Request already in progress", nullptr));
}
self->exit_application_method_call =
FL_METHOD_CALL(g_object_ref(method_call));

// Requested to immediately quit.
if (g_str_equal(type, kExitTypeRequired)) {
quit_application();
g_autoptr(FlValue) exit_result = fl_value_new_map();
fl_value_set_string_take(exit_result, kExitResponseKey,
fl_value_new_string(kExitResponseExit));
return FL_METHOD_RESPONSE(fl_method_success_response_new(exit_result));
}

// Send the request back to Flutter to follow the standard process.
request_app_exit(self, type);

// Will respond later.
return nullptr;
}

// Called when Flutter wants to play a sound.
static FlMethodResponse* system_sound_play(FlPlatformPlugin* self,
FlValue* args) {
Expand All @@ -165,14 +309,7 @@ static FlMethodResponse* system_sound_play(FlPlatformPlugin* self,

// Called when Flutter wants to quit the application.
static FlMethodResponse* system_navigator_pop(FlPlatformPlugin* self) {
GApplication* app = g_application_get_default();
if (app == nullptr) {
return FL_METHOD_RESPONSE(fl_method_error_response_new(
kFailedError, "Unable to get GApplication", nullptr));
}

g_application_quit(app);

quit_application();
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}

Expand All @@ -192,6 +329,8 @@ static void method_call_cb(FlMethodChannel* channel,
response = clipboard_get_data_async(self, method_call);
} else if (strcmp(method, kClipboardHasStringsMethod) == 0) {
response = clipboard_has_strings_async(self, method_call);
} else if (strcmp(method, kExitApplicationMethod) == 0) {
response = system_exit_application(self, method_call);
} else if (strcmp(method, kPlaySoundMethod) == 0) {
response = system_sound_play(self, args);
} else if (strcmp(method, kSystemNavigatorPopMethod) == 0) {
Expand All @@ -208,7 +347,11 @@ static void method_call_cb(FlMethodChannel* channel,
static void fl_platform_plugin_dispose(GObject* object) {
FlPlatformPlugin* self = FL_PLATFORM_PLUGIN(object);

g_cancellable_cancel(self->cancellable);

g_clear_object(&self->channel);
g_clear_object(&self->exit_application_method_call);
g_clear_object(&self->cancellable);

G_OBJECT_CLASS(fl_platform_plugin_parent_class)->dispose(object);
}
Expand All @@ -217,7 +360,9 @@ static void fl_platform_plugin_class_init(FlPlatformPluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = fl_platform_plugin_dispose;
}

static void fl_platform_plugin_init(FlPlatformPlugin* self) {}
static void fl_platform_plugin_init(FlPlatformPlugin* self) {
self->cancellable = g_cancellable_new();
}

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

return self;
}

void fl_platform_plugin_request_app_exit(FlPlatformPlugin* self) {
g_return_if_fail(FL_IS_PLATFORM_PLUGIN(self));
// Request a cancellable exit.
request_app_exit(self, kExitTypeCancelable);
}
9 changes: 9 additions & 0 deletions shell/platform/linux/fl_platform_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ G_DECLARE_FINAL_TYPE(FlPlatformPlugin,
*/
FlPlatformPlugin* fl_platform_plugin_new(FlBinaryMessenger* messenger);

/**
* fl_platform_plugin_request_app_exit:
* @plugin: an #FlPlatformPlugin
*
* Request the application exits (i.e. due to the window being requested to be
* closed).
*/
void fl_platform_plugin_request_app_exit(FlPlatformPlugin* plugin);

G_END_DECLS

#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_PLATFORM_PLUGIN_H_
86 changes: 86 additions & 0 deletions shell/platform/linux/fl_platform_plugin_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,67 @@ MATCHER_P(SuccessResponse, result, "") {
return false;
}

class MethodCallMatcher {
public:
using is_gtest_matcher = void;

explicit MethodCallMatcher(::testing::Matcher<std::string> name,
::testing::Matcher<FlValue*> args)
: name_(std::move(name)), args_(std::move(args)) {}

bool MatchAndExplain(GBytes* method_call,
::testing::MatchResultListener* result_listener) const {
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GError) error = nullptr;
g_autofree gchar* name = nullptr;
g_autoptr(FlValue) args = nullptr;
gboolean result = fl_method_codec_decode_method_call(
FL_METHOD_CODEC(codec), method_call, &name, &args, &error);
if (!result) {
*result_listener << ::testing::PrintToString(error->message);
return false;
}
if (!name_.MatchAndExplain(name, result_listener)) {
*result_listener << " where the name doesn't match: \"" << name << "\"";
return false;
}
if (!args_.MatchAndExplain(args, result_listener)) {
*result_listener << " where the args don't match: "
<< ::testing::PrintToString(args);
return false;
}
return true;
}

void DescribeTo(std::ostream* os) const {
*os << "method name ";
name_.DescribeTo(os);
*os << " and args ";
args_.DescribeTo(os);
}

void DescribeNegationTo(std::ostream* os) const {
*os << "method name ";
name_.DescribeNegationTo(os);
*os << " or args ";
args_.DescribeNegationTo(os);
}

private:
::testing::Matcher<std::string> name_;
::testing::Matcher<FlValue*> args_;
};

static ::testing::Matcher<GBytes*> MethodCall(
const std::string& name,
::testing::Matcher<FlValue*> args) {
return MethodCallMatcher(::testing::StrEq(name), std::move(args));
}

MATCHER_P(FlValueEq, value, "equal to " + ::testing::PrintToString(value)) {
return fl_value_equal(arg, value);
}

TEST(FlPlatformPluginTest, PlaySound) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;

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

messenger.ReceiveMessage("flutter/platform", message);
}

TEST(FlPlatformPluginTest, ExitApplication) {
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;

g_autoptr(FlPlatformPlugin) plugin = fl_platform_plugin_new(messenger);
EXPECT_NE(plugin, nullptr);

g_autoptr(FlValue) args = fl_value_new_map();
fl_value_set_string_take(args, "type", fl_value_new_string("cancelable"));
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
FL_METHOD_CODEC(codec), "System.exitApplication", args, nullptr);

g_autoptr(FlValue) requestArgs = fl_value_new_map();
fl_value_set_string_take(requestArgs, "type",
fl_value_new_string("cancelable"));
EXPECT_CALL(messenger,
fl_binary_messenger_send_on_channel(
::testing::Eq<FlBinaryMessenger*>(messenger),
::testing::StrEq("flutter/platform"),
MethodCall("System.requestAppExit", FlValueEq(requestArgs)),
::testing::_, ::testing::_, ::testing::_));

messenger.ReceiveMessage("flutter/platform", message);
}
14 changes: 14 additions & 0 deletions shell/platform/linux/fl_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ G_DEFINE_TYPE_WITH_CODE(
G_IMPLEMENT_INTERFACE(fl_text_input_view_delegate_get_type(),
fl_view_text_input_delegate_iface_init))

// Signal handler for GtkWidget::delete-event
static gboolean window_delete_event_cb(GtkWidget* widget,
GdkEvent* event,
FlView* self) {
fl_platform_plugin_request_app_exit(self->platform_plugin);
// Stop the event from propagating.
return TRUE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line totally blocks the additional listener provided by plugins, such like window_manager. :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree, it does, but the alternative is returning FALSE, which would not give the app a chance to stop the exit when the last window goes away.

Perhaps this should only make the exit request and return TRUE if there is only one window open. That would still block the plugin listeners on the last window, though.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the change prevents *_plugin_dispose(GObject* object) from being called.

The some_plugin_dispose in the generated template will never be called.

static void some_plugin_dispose(GObject* object) {
  G_OBJECT_CLASS(some_plugin_parent_class)->dispose(object);
}

static void some_plugin_class_init(SomePluginClass* klass) {
  G_OBJECT_CLASS(klass)->dispose = some_plugin_dispose;
}

}

// Initialize keyboard manager.
static void init_keyboard(FlView* self) {
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
Expand Down Expand Up @@ -475,6 +484,11 @@ static void realize_cb(GtkWidget* widget) {
FlView* self = FL_VIEW(widget);
g_autoptr(GError) error = nullptr;

// Handle requests by the user to close the application.
GtkWidget* toplevel_window = gtk_widget_get_toplevel(GTK_WIDGET(self));
g_signal_connect(toplevel_window, "delete-event",
G_CALLBACK(window_delete_event_cb), self);

init_keyboard(self);

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