Add FlKeyEventPlugin (#18313)

diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index ef5efd6..4daa919 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1202,6 +1202,8 @@
 FILE: ../../../flutter/shell/platform/linux/fl_json_message_codec_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec.cc
 FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec_test.cc
+FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.cc
+FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.h
 FILE: ../../../flutter/shell/platform/linux/fl_message_codec.cc
 FILE: ../../../flutter/shell/platform/linux/fl_message_codec_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_method_call.cc
diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn
index d32c572..4782885 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -79,6 +79,7 @@
     "fl_engine.cc",
     "fl_json_message_codec.cc",
     "fl_json_method_codec.cc",
+    "fl_key_event_plugin.cc",
     "fl_message_codec.cc",
     "fl_method_call.cc",
     "fl_method_channel.cc",
diff --git a/shell/platform/linux/fl_key_event_plugin.cc b/shell/platform/linux/fl_key_event_plugin.cc
new file mode 100644
index 0000000..2a436c3
--- /dev/null
+++ b/shell/platform/linux/fl_key_event_plugin.cc
@@ -0,0 +1,255 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h"
+
+static constexpr char kChannelName[] = "flutter/keyevent";
+static constexpr char kTypeKey[] = "type";
+static constexpr char kTypeValueUp[] = "keyup";
+static constexpr char kTypeValueDown[] = "keydown";
+static constexpr char kKeymapKey[] = "keymap";
+static constexpr char kKeyCodeKey[] = "keyCode";
+static constexpr char kScanCodeKey[] = "scanCode";
+static constexpr char kModifiersKey[] = "modifiers";
+static constexpr char kToolkitKey[] = "toolkit";
+static constexpr char kUnicodeScalarValuesKey[] = "unicodeScalarValues";
+
+static constexpr char kGLFWToolkit[] = "glfw";
+static constexpr char kLinuxKeymap[] = "linux";
+
+struct _FlKeyEventPlugin {
+  GObject parent_instance;
+
+  FlBasicMessageChannel* channel;
+};
+
+G_DEFINE_TYPE(FlKeyEventPlugin, fl_key_event_plugin, G_TYPE_OBJECT)
+
+// Converts a Gdk key code to its GLFW equivalent.
+// TODO(robert-ancell) Create a "gtk" toolkit in Flutter so we don't have to
+// convert values. https://github.com/flutter/flutter/issues/57603
+static int gdk_keyval_to_glfw_key_code(guint keyval) {
+  switch (keyval) {
+    case GDK_KEY_space:
+      return 32;
+    case GDK_KEY_apostrophe:
+      return 39;
+    case GDK_KEY_comma:
+      return 44;
+    case GDK_KEY_minus:
+      return 45;
+    case GDK_KEY_period:
+      return 46;
+    case GDK_KEY_slash:
+      return 47;
+    case GDK_KEY_0:
+      return 48;
+    case GDK_KEY_1:
+      return 49;
+    case GDK_KEY_2:
+      return 50;
+    case GDK_KEY_3:
+      return 51;
+    case GDK_KEY_4:
+      return 52;
+    case GDK_KEY_5:
+      return 53;
+    case GDK_KEY_6:
+      return 54;
+    case GDK_KEY_7:
+      return 55;
+    case GDK_KEY_8:
+      return 56;
+    case GDK_KEY_9:
+      return 57;
+    case GDK_KEY_semicolon:
+      return 59;
+    case GDK_KEY_equal:
+      return 61;
+    case GDK_KEY_a:
+      return 65;
+    case GDK_KEY_b:
+      return 66;
+    case GDK_KEY_c:
+      return 67;
+    case GDK_KEY_d:
+      return 68;
+    case GDK_KEY_e:
+      return 69;
+    case GDK_KEY_f:
+      return 70;
+    case GDK_KEY_g:
+      return 71;
+    case GDK_KEY_h:
+      return 72;
+    case GDK_KEY_i:
+      return 73;
+    case GDK_KEY_j:
+      return 74;
+    case GDK_KEY_k:
+      return 75;
+    case GDK_KEY_l:
+      return 76;
+    case GDK_KEY_m:
+      return 77;
+    case GDK_KEY_n:
+      return 78;
+    case GDK_KEY_o:
+      return 79;
+    case GDK_KEY_p:
+      return 80;
+    case GDK_KEY_q:
+      return 81;
+    case GDK_KEY_r:
+      return 82;
+    case GDK_KEY_s:
+      return 83;
+    case GDK_KEY_t:
+      return 84;
+    case GDK_KEY_u:
+      return 85;
+    case GDK_KEY_v:
+      return 86;
+    case GDK_KEY_w:
+      return 87;
+    case GDK_KEY_x:
+      return 88;
+    case GDK_KEY_y:
+      return 89;
+    case GDK_KEY_z:
+      return 90;
+    case GDK_KEY_bracketleft:
+      return 91;
+    case GDK_KEY_bracketright:
+      return 92;
+    case GDK_KEY_grave:
+      return 96;
+    case GDK_KEY_Escape:
+      return 256;
+    case GDK_KEY_Return:
+      return 257;
+    case GDK_KEY_Tab:
+      return 258;
+    case GDK_KEY_BackSpace:
+      return 259;
+    case GDK_KEY_Insert:
+      return 260;
+    case GDK_KEY_Delete:
+      return 261;
+    case GDK_KEY_Right:
+      return 262;
+    case GDK_KEY_Left:
+      return 263;
+    case GDK_KEY_Down:
+      return 264;
+    case GDK_KEY_Up:
+      return 265;
+    case GDK_KEY_Page_Up:
+      return 266;
+    case GDK_KEY_Page_Down:
+      return 267;
+    case GDK_KEY_Home:
+      return 268;
+    case GDK_KEY_End:
+      return 269;
+    case GDK_KEY_Shift_L:
+      return 340;
+    case GDK_KEY_Control_L:
+      return 341;
+    case GDK_KEY_Alt_L:
+      return 342;
+    case GDK_KEY_Super_L:
+      return 343;
+    case GDK_KEY_Shift_R:
+      return 344;
+    case GDK_KEY_Control_R:
+      return 345;
+    case GDK_KEY_Alt_R:
+      return 346;
+    case GDK_KEY_Super_R:
+      return 347;
+    default:
+      return 0;
+  }
+}
+
+// Converts a Gdk key state to its GLFW equivalent.
+int64_t gdk_state_to_glfw_modifiers(guint8 state) {
+  int64_t modifiers = 0;
+
+  if ((state & GDK_SHIFT_MASK) != 0)
+    modifiers |= 0x0001;
+  if ((state & GDK_CONTROL_MASK) != 0)
+    modifiers |= 0x0002;
+  if ((state & GDK_MOD1_MASK) != 0)
+    modifiers |= 0x0004;
+  if ((state & GDK_SUPER_MASK) != 0)
+    modifiers |= 0x0008;
+
+  return modifiers;
+}
+
+static void fl_key_event_plugin_dispose(GObject* object) {
+  FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(object);
+
+  g_clear_object(&self->channel);
+
+  G_OBJECT_CLASS(fl_key_event_plugin_parent_class)->dispose(object);
+}
+
+static void fl_key_event_plugin_class_init(FlKeyEventPluginClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_key_event_plugin_dispose;
+}
+
+static void fl_key_event_plugin_init(FlKeyEventPlugin* self) {}
+
+FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger) {
+  g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
+
+  FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(
+      g_object_new(fl_key_event_plugin_get_type(), nullptr));
+
+  g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new();
+  self->channel = fl_basic_message_channel_new(messenger, kChannelName,
+                                               FL_MESSAGE_CODEC(codec));
+
+  return self;
+}
+
+void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
+                                        GdkEventKey* event) {
+  g_return_if_fail(FL_IS_KEY_EVENT_PLUGIN(self));
+  g_return_if_fail(event != nullptr);
+
+  const gchar* type;
+  if (event->type == GDK_KEY_PRESS)
+    type = kTypeValueDown;
+  else if (event->type == GDK_KEY_RELEASE)
+    type = kTypeValueUp;
+  else
+    return;
+
+  int64_t scan_code = event->hardware_keycode;
+  int64_t key_code = gdk_keyval_to_glfw_key_code(event->keyval);
+  int64_t modifiers = gdk_state_to_glfw_modifiers(event->state);
+  int64_t unicodeScalarValues = gdk_keyval_to_unicode(event->keyval);
+
+  g_autoptr(FlValue) message = fl_value_new_map();
+  fl_value_set_string_take(message, kTypeKey, fl_value_new_string(type));
+  fl_value_set_string_take(message, kKeymapKey,
+                           fl_value_new_string(kLinuxKeymap));
+  fl_value_set_string_take(message, kScanCodeKey, fl_value_new_int(scan_code));
+  fl_value_set_string_take(message, kToolkitKey,
+                           fl_value_new_string(kGLFWToolkit));
+  fl_value_set_string_take(message, kKeyCodeKey, fl_value_new_int(key_code));
+  fl_value_set_string_take(message, kModifiersKey, fl_value_new_int(modifiers));
+  if (unicodeScalarValues != 0)
+    fl_value_set_string_take(message, kUnicodeScalarValuesKey,
+                             fl_value_new_int(unicodeScalarValues));
+
+  fl_basic_message_channel_send(self->channel, message, nullptr, nullptr,
+                                nullptr);
+}
diff --git a/shell/platform/linux/fl_key_event_plugin.h b/shell/platform/linux/fl_key_event_plugin.h
new file mode 100644
index 0000000..dee212d
--- /dev/null
+++ b/shell/platform/linux/fl_key_event_plugin.h
@@ -0,0 +1,49 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_
+#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_
+
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
+
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(FlKeyEventPlugin,
+                     fl_key_event_plugin,
+                     FL,
+                     KEY_EVENT_PLUGIN,
+                     GObject);
+
+/**
+ * FlKeyEventPlugin:
+ *
+ * #FlKeyEventPlugin is a plugin that implements the shell side
+ * of SystemChannels.keyEvent from the Flutter services library.
+ */
+
+/**
+ * fl_key_event_plugin_new:
+ * @messenger: an #FlBinaryMessenger.
+ *
+ * Creates a #FlKeyEventPlugin.
+ *
+ * Returns: a new #FlKeyEventPlugin.
+ */
+FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger);
+
+/**
+ * fl_key_event_plugin_send_key_event:
+ * @plugin: an #FlKeyEventPlugin.
+ * @event: a #GdkEventKey.
+ *
+ * Sends a key event to Flutter.
+ */
+void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin,
+                                        GdkEventKey* event);
+
+G_END_DECLS
+
+#endif  // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_
diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc
index 1c5bdbe..3b1c2c7 100644
--- a/shell/platform/linux/fl_view.cc
+++ b/shell/platform/linux/fl_view.cc
@@ -5,6 +5,7 @@
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"
 
 #include "flutter/shell/platform/linux/fl_engine_private.h"
+#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
 #include "flutter/shell/platform/linux/fl_renderer_x11.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
 
@@ -26,13 +27,16 @@
 
   // Pointer button state recorded for sending status updates
   int64_t button_state;
+
+  // Flutter system channel handlers.
+  FlKeyEventPlugin* key_event_plugin;
 };
 
 enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST };
 
 G_DEFINE_TYPE(FlView, fl_view, GTK_TYPE_WIDGET)
 
-// Convert a GDK button event into a Flutter event and send to the engine
+// Converts a GDK button event into a Flutter event and sends it to the engine.
 static gboolean fl_view_send_pointer_button_event(FlView* self,
                                                   GdkEventButton* event) {
   int64_t button;
@@ -81,6 +85,10 @@
 
   self->renderer = fl_renderer_x11_new();
   self->engine = fl_engine_new(self->project, FL_RENDERER(self->renderer));
+
+  // Create system channel handlers
+  FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
+  self->key_event_plugin = fl_key_event_plugin_new(messenger);
 }
 
 static void fl_view_set_property(GObject* object,
@@ -122,6 +130,7 @@
   g_clear_object(&self->project);
   g_clear_object(&self->renderer);
   g_clear_object(&self->engine);
+  g_clear_object(&self->key_event_plugin);
 
   G_OBJECT_CLASS(fl_view_parent_class)->dispose(object);
 }
@@ -145,7 +154,8 @@
   window_attributes.visual = gtk_widget_get_visual(widget);
   window_attributes.event_mask =
       gtk_widget_get_events(widget) | GDK_EXPOSURE_MASK |
-      GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK;
+      GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK |
+      GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK;
 
   gint window_attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
 
@@ -217,6 +227,25 @@
   return TRUE;
 }
 
+// Implements GtkWidget::key_press_event
+static gboolean fl_view_key_press_event(GtkWidget* widget, GdkEventKey* event) {
+  FlView* self = FL_VIEW(widget);
+
+  fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
+
+  return TRUE;
+}
+
+// Implements GtkWidget::key_release_event
+static gboolean fl_view_key_release_event(GtkWidget* widget,
+                                          GdkEventKey* event) {
+  FlView* self = FL_VIEW(widget);
+
+  fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
+
+  return TRUE;
+}
+
 static void fl_view_class_init(FlViewClass* klass) {
   G_OBJECT_CLASS(klass)->constructed = fl_view_constructed;
   G_OBJECT_CLASS(klass)->set_property = fl_view_set_property;
@@ -227,6 +256,8 @@
   GTK_WIDGET_CLASS(klass)->button_press_event = fl_view_button_press_event;
   GTK_WIDGET_CLASS(klass)->button_release_event = fl_view_button_release_event;
   GTK_WIDGET_CLASS(klass)->motion_notify_event = fl_view_motion_notify_event;
+  GTK_WIDGET_CLASS(klass)->key_press_event = fl_view_key_press_event;
+  GTK_WIDGET_CLASS(klass)->key_release_event = fl_view_key_release_event;
 
   g_object_class_install_property(
       G_OBJECT_CLASS(klass), PROP_FLUTTER_PROJECT,
@@ -237,7 +268,9 @@
                                    G_PARAM_STATIC_STRINGS)));
 }
 
-static void fl_view_init(FlView* self) {}
+static void fl_view_init(FlView* self) {
+  gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
+}
 
 G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) {
   return static_cast<FlView*>(