Make fl_engine_send_key_event into a standard async function. (#57112)

Add missing tests for this function.

Note this makes FlKeyboardManager a bit more complex, but this is
planned to be simplified in a future refactor.
diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc
index 2ca7f2d..eb3d678 100644
--- a/shell/platform/linux/fl_engine.cc
+++ b/shell/platform/linux/fl_engine.cc
@@ -1095,17 +1095,51 @@
   self->embedder_api.SendPointerEvent(self->engine, &fl_event, 1);
 }
 
+static void send_key_event_cb(bool handled, void* user_data) {
+  g_autoptr(GTask) task = G_TASK(user_data);
+  gboolean* return_value = g_new0(gboolean, 1);
+  *return_value = handled;
+  g_task_return_pointer(task, return_value, g_free);
+}
+
 void fl_engine_send_key_event(FlEngine* self,
                               const FlutterKeyEvent* event,
-                              FlutterKeyEventCallback callback,
-                              void* user_data) {
+                              GCancellable* cancellable,
+                              GAsyncReadyCallback callback,
+                              gpointer user_data) {
   g_return_if_fail(FL_IS_ENGINE(self));
 
+  g_autoptr(GTask) task = g_task_new(self, cancellable, callback, user_data);
+
   if (self->engine == nullptr) {
+    g_task_return_new_error(task, fl_engine_error_quark(),
+                            FL_ENGINE_ERROR_FAILED, "No engine");
     return;
   }
 
-  self->embedder_api.SendKeyEvent(self->engine, event, callback, user_data);
+  if (self->embedder_api.SendKeyEvent(self->engine, event, send_key_event_cb,
+                                      g_object_ref(task)) != kSuccess) {
+    g_task_return_new_error(task, fl_engine_error_quark(),
+                            FL_ENGINE_ERROR_FAILED, "Failed to send key event");
+    g_object_unref(task);
+  }
+}
+
+gboolean fl_engine_send_key_event_finish(FlEngine* self,
+                                         GAsyncResult* result,
+                                         gboolean* handled,
+                                         GError** error) {
+  g_return_val_if_fail(FL_IS_ENGINE(self), FALSE);
+  g_return_val_if_fail(g_task_is_valid(result, self), FALSE);
+
+  g_autofree gboolean* return_value =
+      static_cast<gboolean*>(g_task_propagate_pointer(G_TASK(result), error));
+  if (return_value == nullptr) {
+    return FALSE;
+  }
+
+  *handled = *return_value;
+  return TRUE;
 }
 
 void fl_engine_dispatch_semantics_action(FlEngine* self,
diff --git a/shell/platform/linux/fl_engine_private.h b/shell/platform/linux/fl_engine_private.h
index df56941..96e9b6b 100644
--- a/shell/platform/linux/fl_engine_private.h
+++ b/shell/platform/linux/fl_engine_private.h
@@ -369,11 +369,37 @@
 
 /**
  * fl_engine_send_key_event:
+ * @engine: an #FlEngine.
+ * @event: key event to send.
+ * @cancellable: (allow-none): a #GCancellable or %NULL.
+ * @callback: (scope async): a #GAsyncReadyCallback to call when the request is
+ * satisfied.
+ * @user_data: (closure): user data to pass to @callback.
+ *
+ * Send a key event to the engine.
  */
 void fl_engine_send_key_event(FlEngine* engine,
                               const FlutterKeyEvent* event,
-                              FlutterKeyEventCallback callback,
-                              void* user_data);
+                              GCancellable* cancellable,
+                              GAsyncReadyCallback callback,
+                              gpointer user_data);
+
+/**
+ * fl_engine_send_key_event_finish:
+ * @engine: an #FlEngine.
+ * @result: a #GAsyncResult.
+ * @handled: location to write if this event was handled by the engine.
+ * @error: (allow-none): #GError location to store the error occurring, or %NULL
+ * to ignore.
+ *
+ * Completes request started with fl_engine_send_key_event().
+ *
+ * Returns: %TRUE on success.
+ */
+gboolean fl_engine_send_key_event_finish(FlEngine* engine,
+                                         GAsyncResult* result,
+                                         gboolean* handled,
+                                         GError** error);
 
 /**
  * fl_engine_dispatch_semantics_action:
diff --git a/shell/platform/linux/fl_engine_test.cc b/shell/platform/linux/fl_engine_test.cc
index 62fcb48..083b649 100644
--- a/shell/platform/linux/fl_engine_test.cc
+++ b/shell/platform/linux/fl_engine_test.cc
@@ -748,4 +748,132 @@
   g_main_loop_run(loop);
 }
 
+TEST(FlEngineTest, SendKeyEvent) {
+  g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
+
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine);
+
+  bool called;
+  embedder_api->SendKeyEvent = MOCK_ENGINE_PROC(
+      SendKeyEvent,
+      ([&called](auto engine, const FlutterKeyEvent* event,
+                 FlutterKeyEventCallback callback, void* user_data) {
+        called = true;
+        EXPECT_EQ(event->timestamp, 1234);
+        EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
+        EXPECT_EQ(event->physical, static_cast<uint64_t>(42));
+        EXPECT_EQ(event->logical, static_cast<uint64_t>(123));
+        EXPECT_TRUE(event->synthesized);
+        EXPECT_EQ(event->device_type, kFlutterKeyEventDeviceTypeKeyboard);
+        callback(TRUE, user_data);
+        return kSuccess;
+      }));
+
+  FlutterKeyEvent event = {.struct_size = sizeof(FlutterKeyEvent),
+                           .timestamp = 1234,
+                           .type = kFlutterKeyEventTypeUp,
+                           .physical = 42,
+                           .logical = 123,
+                           .character = nullptr,
+                           .synthesized = true,
+                           .device_type = kFlutterKeyEventDeviceTypeKeyboard};
+  fl_engine_send_key_event(
+      engine, &event, nullptr,
+      [](GObject* object, GAsyncResult* result, gpointer user_data) {
+        gboolean handled;
+        g_autoptr(GError) error = nullptr;
+        EXPECT_TRUE(fl_engine_send_key_event_finish(FL_ENGINE(object), result,
+                                                    &handled, &error));
+        EXPECT_EQ(error, nullptr);
+        EXPECT_TRUE(handled);
+        g_main_loop_quit(static_cast<GMainLoop*>(user_data));
+      },
+      loop);
+
+  g_main_loop_run(loop);
+  EXPECT_TRUE(called);
+}
+
+TEST(FlEngineTest, SendKeyEventNotHandled) {
+  g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
+
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine);
+
+  bool called;
+  embedder_api->SendKeyEvent = MOCK_ENGINE_PROC(
+      SendKeyEvent,
+      ([&called](auto engine, const FlutterKeyEvent* event,
+                 FlutterKeyEventCallback callback, void* user_data) {
+        called = true;
+        callback(FALSE, user_data);
+        return kSuccess;
+      }));
+
+  FlutterKeyEvent event = {.struct_size = sizeof(FlutterKeyEvent),
+                           .timestamp = 1234,
+                           .type = kFlutterKeyEventTypeUp,
+                           .physical = 42,
+                           .logical = 123,
+                           .character = nullptr,
+                           .synthesized = true,
+                           .device_type = kFlutterKeyEventDeviceTypeKeyboard};
+  fl_engine_send_key_event(
+      engine, &event, nullptr,
+      [](GObject* object, GAsyncResult* result, gpointer user_data) {
+        gboolean handled;
+        g_autoptr(GError) error = nullptr;
+        EXPECT_TRUE(fl_engine_send_key_event_finish(FL_ENGINE(object), result,
+                                                    &handled, &error));
+        EXPECT_EQ(error, nullptr);
+        EXPECT_FALSE(handled);
+        g_main_loop_quit(static_cast<GMainLoop*>(user_data));
+      },
+      loop);
+
+  g_main_loop_run(loop);
+  EXPECT_TRUE(called);
+}
+
+TEST(FlEngineTest, SendKeyEventError) {
+  g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
+
+  g_autoptr(FlEngine) engine = make_mock_engine();
+  FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine);
+
+  bool called;
+  embedder_api->SendKeyEvent = MOCK_ENGINE_PROC(
+      SendKeyEvent,
+      ([&called](auto engine, const FlutterKeyEvent* event,
+                 FlutterKeyEventCallback callback, void* user_data) {
+        called = true;
+        return kInvalidArguments;
+      }));
+
+  FlutterKeyEvent event = {.struct_size = sizeof(FlutterKeyEvent),
+                           .timestamp = 1234,
+                           .type = kFlutterKeyEventTypeUp,
+                           .physical = 42,
+                           .logical = 123,
+                           .character = nullptr,
+                           .synthesized = true,
+                           .device_type = kFlutterKeyEventDeviceTypeKeyboard};
+  fl_engine_send_key_event(
+      engine, &event, nullptr,
+      [](GObject* object, GAsyncResult* result, gpointer user_data) {
+        gboolean handled;
+        g_autoptr(GError) error = nullptr;
+        EXPECT_FALSE(fl_engine_send_key_event_finish(FL_ENGINE(object), result,
+                                                     &handled, &error));
+        EXPECT_NE(error, nullptr);
+        EXPECT_STREQ(error->message, "Failed to send key event");
+        g_main_loop_quit(static_cast<GMainLoop*>(user_data));
+      },
+      loop);
+
+  g_main_loop_run(loop);
+  EXPECT_TRUE(called);
+}
+
 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
diff --git a/shell/platform/linux/fl_keyboard_manager.cc b/shell/platform/linux/fl_keyboard_manager.cc
index 0a49257..3e84c73 100644
--- a/shell/platform/linux/fl_keyboard_manager.cc
+++ b/shell/platform/linux/fl_keyboard_manager.cc
@@ -9,6 +9,7 @@
 #include <memory>
 #include <string>
 
+#include "flutter/shell/platform/linux/fl_engine_private.h"
 #include "flutter/shell/platform/linux/fl_key_channel_responder.h"
 #include "flutter/shell/platform/linux/fl_key_embedder_responder.h"
 #include "flutter/shell/platform/linux/fl_keyboard_layout.h"
@@ -479,8 +480,35 @@
         } else {
           g_autoptr(FlEngine) engine = FL_ENGINE(g_weak_ref_get(&self->engine));
           if (engine != nullptr) {
-            fl_engine_send_key_event(engine, event, callback,
-                                     callback_user_data);
+            typedef struct {
+              FlutterKeyEventCallback callback;
+              void* callback_user_data;
+            } SendKeyEventData;
+            SendKeyEventData* data = g_new0(SendKeyEventData, 1);
+            data->callback = callback;
+            data->callback_user_data = callback_user_data;
+            fl_engine_send_key_event(
+                engine, event, self->cancellable,
+                [](GObject* object, GAsyncResult* result, gpointer user_data) {
+                  g_autofree SendKeyEventData* data =
+                      static_cast<SendKeyEventData*>(user_data);
+                  gboolean handled = FALSE;
+                  g_autoptr(GError) error = nullptr;
+                  if (!fl_engine_send_key_event_finish(
+                          FL_ENGINE(object), result, &handled, &error)) {
+                    if (g_error_matches(error, G_IO_ERROR,
+                                        G_IO_ERROR_CANCELLED)) {
+                      return;
+                    }
+
+                    g_warning("Failed to send key event: %s", error->message);
+                  }
+
+                  if (data->callback != nullptr) {
+                    data->callback(handled, data->callback_user_data);
+                  }
+                },
+                data);
           }
         }
       },