Use a AT-SPI socket/plug to export the Flutter accessibility state. (#52355)

This will be useful when using GTK4, which doesn't support custom a11y
code in GTK and will require the Flutter code to be exported in a plug.
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 45ac54a..2daa27b 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -42442,6 +42442,8 @@
 ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_settings_portal_test.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_socket_accessible.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_socket_accessible.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_message_codec.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_message_codec_test.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc + ../../../flutter/LICENSE
@@ -45348,6 +45350,8 @@
 FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.cc
 FILE: ../../../flutter/shell/platform/linux/fl_settings_portal.h
 FILE: ../../../flutter/shell/platform/linux/fl_settings_portal_test.cc
+FILE: ../../../flutter/shell/platform/linux/fl_socket_accessible.cc
+FILE: ../../../flutter/shell/platform/linux/fl_socket_accessible.h
 FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec.cc
 FILE: ../../../flutter/shell/platform/linux/fl_standard_message_codec_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc
diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn
index c28c7b5..8e1b8fc 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -132,6 +132,7 @@
     "fl_settings.cc",
     "fl_settings_plugin.cc",
     "fl_settings_portal.cc",
+    "fl_socket_accessible.cc",
     "fl_standard_message_codec.cc",
     "fl_standard_method_codec.cc",
     "fl_string_codec.cc",
diff --git a/shell/platform/linux/fl_socket_accessible.cc b/shell/platform/linux/fl_socket_accessible.cc
new file mode 100644
index 0000000..9b2d06c
--- /dev/null
+++ b/shell/platform/linux/fl_socket_accessible.cc
@@ -0,0 +1,57 @@
+// 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_socket_accessible.h"
+
+// This is a copy of GtkSocketAccessible, which requires GTK 3.24.30
+
+struct _FlSocketAccessible {
+  GtkContainerAccessible parent;
+  AtkObject* accessible_socket;
+};
+
+G_DEFINE_TYPE(FlSocketAccessible,
+              fl_socket_accessible,
+              GTK_TYPE_CONTAINER_ACCESSIBLE)
+
+static AtkObject* fl_socket_accessible_ref_child(AtkObject* object, int i) {
+  FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object);
+  return i == 0 ? ATK_OBJECT(g_object_ref(self->accessible_socket)) : nullptr;
+}
+
+static int fl_socket_accessible_get_n_children(AtkObject* object) {
+  return 1;
+}
+
+static void fl_socket_accessible_finalize(GObject* object) {
+  FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object);
+
+  g_clear_object(&self->accessible_socket);
+
+  G_OBJECT_CLASS(fl_socket_accessible_parent_class)->finalize(object);
+}
+
+static void fl_socket_accessible_initialize(AtkObject* object, gpointer data) {
+  FlSocketAccessible* self = FL_SOCKET_ACCESSIBLE(object);
+
+  ATK_OBJECT_CLASS(fl_socket_accessible_parent_class)->initialize(object, data);
+
+  self->accessible_socket = atk_socket_new();
+}
+
+static void fl_socket_accessible_class_init(FlSocketAccessibleClass* klass) {
+  GObjectClass* object_class = G_OBJECT_CLASS(klass);
+  object_class->finalize = fl_socket_accessible_finalize;
+
+  AtkObjectClass* atk_class = ATK_OBJECT_CLASS(klass);
+  atk_class->initialize = fl_socket_accessible_initialize;
+  atk_class->get_n_children = fl_socket_accessible_get_n_children;
+  atk_class->ref_child = fl_socket_accessible_ref_child;
+}
+
+static void fl_socket_accessible_init(FlSocketAccessible* self) {}
+
+void fl_socket_accessible_embed(FlSocketAccessible* self, gchar* id) {
+  atk_socket_embed(ATK_SOCKET(self->accessible_socket), id);
+}
diff --git a/shell/platform/linux/fl_socket_accessible.h b/shell/platform/linux/fl_socket_accessible.h
new file mode 100644
index 0000000..a3eb6ed
--- /dev/null
+++ b/shell/platform/linux/fl_socket_accessible.h
@@ -0,0 +1,22 @@
+// 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_SOCKET_ACCESSIBLE_H_
+#define FLUTTER_SHELL_PLATFORM_LINUX_FL_SOCKET_ACCESSIBLE_H_
+
+#include <gtk/gtk-a11y.h>
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(FlSocketAccessible,
+                     fl_socket_accessible,
+                     FL,
+                     SOCKET_ACCESSIBLE,
+                     GtkContainerAccessible);
+
+void fl_socket_accessible_embed(FlSocketAccessible* self, gchar* id);
+
+G_END_DECLS
+
+#endif  // FLUTTER_SHELL_PLATFORM_LINUX_FL_SOCKET_ACCESSIBLE_H_
diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc
index 0d1fb47..8dc057e 100644
--- a/shell/platform/linux/fl_view.cc
+++ b/shell/platform/linux/fl_view.cc
@@ -6,8 +6,12 @@
 
 #include "flutter/shell/platform/linux/fl_view_private.h"
 
+#include <atk/atk.h>
+#include <gtk/gtk-a11y.h>
+
 #include <cstring>
 
+#include "flutter/shell/platform/linux/fl_accessible_node.h"
 #include "flutter/shell/platform/linux/fl_backing_store_provider.h"
 #include "flutter/shell/platform/linux/fl_engine_private.h"
 #include "flutter/shell/platform/linux/fl_key_event.h"
@@ -19,6 +23,7 @@
 #include "flutter/shell/platform/linux/fl_renderer_gdk.h"
 #include "flutter/shell/platform/linux/fl_scrolling_manager.h"
 #include "flutter/shell/platform/linux/fl_scrolling_view_delegate.h"
+#include "flutter/shell/platform/linux/fl_socket_accessible.h"
 #include "flutter/shell/platform/linux/fl_text_input_plugin.h"
 #include "flutter/shell/platform/linux/fl_text_input_view_delegate.h"
 #include "flutter/shell/platform/linux/fl_view_accessible.h"
@@ -64,6 +69,9 @@
   gulong keymap_keys_changed_cb_id;  // Signal connection ID for
                                      // keymap-keys-changed
   gulong window_state_cb_id;  // Signal connection ID for window-state-changed
+
+  // Accessible tree from Flutter, exposed as an AtkPlug.
+  FlViewAccessible* view_accessible;
 };
 
 enum { kPropFlutterProject = 1, kPropLast };
@@ -231,9 +239,7 @@
                                 gpointer user_data) {
   FlView* self = FL_VIEW(user_data);
 
-  AtkObject* accessible = gtk_widget_get_accessible(GTK_WIDGET(self));
-  fl_view_accessible_handle_update_semantics(FL_VIEW_ACCESSIBLE(accessible),
-                                             update);
+  fl_view_accessible_handle_update_semantics(self->view_accessible, update);
 }
 
 // Invoked by the engine right before the engine is restarted.
@@ -586,6 +592,11 @@
   }
 
   handle_geometry_changed(self);
+
+  self->view_accessible = fl_view_accessible_new(self->engine);
+  fl_socket_accessible_embed(
+      FL_SOCKET_ACCESSIBLE(gtk_widget_get_accessible(GTK_WIDGET(self))),
+      atk_plug_get_id(ATK_PLUG(self->view_accessible)));
 }
 
 static gboolean render_cb(FlView* self, GdkGLContext* context) {
@@ -691,6 +702,7 @@
   }
   g_clear_object(&self->mouse_cursor_plugin);
   g_clear_object(&self->platform_plugin);
+  g_clear_object(&self->view_accessible);
 
   G_OBJECT_CLASS(fl_view_parent_class)->dispose(object);
 }
@@ -733,7 +745,7 @@
                                    G_PARAM_STATIC_STRINGS)));
 
   gtk_widget_class_set_accessible_type(GTK_WIDGET_CLASS(klass),
-                                       fl_view_accessible_get_type());
+                                       fl_socket_accessible_get_type());
 }
 
 static void fl_view_init(FlView* self) {
diff --git a/shell/platform/linux/fl_view_accessible.cc b/shell/platform/linux/fl_view_accessible.cc
index 7ddefe0..77ba12a 100644
--- a/shell/platform/linux/fl_view_accessible.cc
+++ b/shell/platform/linux/fl_view_accessible.cc
@@ -8,45 +8,31 @@
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"
 
+static constexpr int32_t kRootSemanticsNodeId = 0;
+
 struct _FlViewAccessible {
-  GtkContainerAccessible parent_instance;
+  AtkPlug parent_instance;
 
   FlEngine* engine;
 
   // Semantics nodes keyed by ID
   GHashTable* semantics_nodes_by_id;
+
+  // Flag to track when root node is created.
+  gboolean root_node_created;
 };
 
 enum { kProp0, kPropEngine, kPropLast };
 
-G_DEFINE_TYPE(FlViewAccessible,
-              fl_view_accessible,
-              GTK_TYPE_CONTAINER_ACCESSIBLE)
-
-static void init_engine(FlViewAccessible* self, FlEngine* engine) {
-  g_assert(self->engine == nullptr);
-  self->engine = engine;
-  g_object_add_weak_pointer(G_OBJECT(self),
-                            reinterpret_cast<gpointer*>(&self->engine));
-}
-
-static FlEngine* get_engine(FlViewAccessible* self) {
-  if (self->engine == nullptr) {
-    FlView* view = FL_VIEW(gtk_accessible_get_widget(GTK_ACCESSIBLE(self)));
-    init_engine(self, fl_view_get_engine(view));
-  }
-  return self->engine;
-}
+G_DEFINE_TYPE(FlViewAccessible, fl_view_accessible, ATK_TYPE_PLUG)
 
 static FlAccessibleNode* create_node(FlViewAccessible* self,
                                      FlutterSemanticsNode2* semantics) {
-  FlEngine* engine = get_engine(self);
-
   if (semantics->flags & kFlutterSemanticsFlagIsTextField) {
-    return fl_accessible_text_field_new(engine, semantics->id);
+    return fl_accessible_text_field_new(self->engine, semantics->id);
   }
 
-  return fl_accessible_node_new(engine, semantics->id);
+  return fl_accessible_node_new(self->engine, semantics->id);
 }
 
 static FlAccessibleNode* lookup_node(FlViewAccessible* self, int32_t id) {
@@ -64,14 +50,19 @@
   }
 
   node = create_node(self, semantics);
-  if (semantics->id == 0) {
+  if (semantics->id == kRootSemanticsNodeId) {
     fl_accessible_node_set_parent(node, ATK_OBJECT(self), 0);
-    g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr);
   }
   g_hash_table_insert(self->semantics_nodes_by_id,
                       GINT_TO_POINTER(semantics->id),
                       reinterpret_cast<gpointer>(node));
 
+  // Update when root node is created.
+  if (!self->root_node_created && semantics->id == kRootSemanticsNodeId) {
+    g_signal_emit_by_name(self, "children-changed::add", 0, node, nullptr);
+    self->root_node_created = true;
+  }
+
   return node;
 }
 
@@ -104,20 +95,11 @@
   return ATK_ROLE_PANEL;
 }
 
-// Implements GObject::set_property
-static void fl_view_accessible_set_property(GObject* object,
-                                            guint prop_id,
-                                            const GValue* value,
-                                            GParamSpec* pspec) {
-  FlViewAccessible* self = FL_VIEW_ACCESSIBLE(object);
-  switch (prop_id) {
-    case kPropEngine:
-      init_engine(self, FL_ENGINE(g_value_get_object(value)));
-      break;
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
-      break;
-  }
+// Implements AtkObject::ref_state_set
+static AtkStateSet* fl_view_accessible_ref_state_set(AtkObject* accessible) {
+  FlViewAccessible* self = FL_VIEW_ACCESSIBLE(accessible);
+  FlAccessibleNode* node = lookup_node(self, 0);
+  return node != nullptr ? atk_object_ref_state_set(ATK_OBJECT(node)) : nullptr;
 }
 
 static void fl_view_accessible_dispose(GObject* object) {
@@ -138,16 +120,9 @@
   ATK_OBJECT_CLASS(klass)->get_n_children = fl_view_accessible_get_n_children;
   ATK_OBJECT_CLASS(klass)->ref_child = fl_view_accessible_ref_child;
   ATK_OBJECT_CLASS(klass)->get_role = fl_view_accessible_get_role;
+  ATK_OBJECT_CLASS(klass)->ref_state_set = fl_view_accessible_ref_state_set;
 
   G_OBJECT_CLASS(klass)->dispose = fl_view_accessible_dispose;
-  G_OBJECT_CLASS(klass)->set_property = fl_view_accessible_set_property;
-
-  g_object_class_install_property(
-      G_OBJECT_CLASS(klass), kPropEngine,
-      g_param_spec_object(
-          "engine", "engine", "Flutter engine", fl_engine_get_type(),
-          static_cast<GParamFlags>(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
-                                   G_PARAM_STATIC_STRINGS)));
 }
 
 static void fl_view_accessible_init(FlViewAccessible* self) {
@@ -155,13 +130,21 @@
       g_direct_hash, g_direct_equal, nullptr, g_object_unref);
 }
 
+FlViewAccessible* fl_view_accessible_new(FlEngine* engine) {
+  FlViewAccessible* self =
+      FL_VIEW_ACCESSIBLE(g_object_new(fl_view_accessible_get_type(), nullptr));
+  self->engine = engine;
+  g_object_add_weak_pointer(G_OBJECT(self),
+                            reinterpret_cast<gpointer*>(&self->engine));
+  return self;
+}
+
 void fl_view_accessible_handle_update_semantics(
     FlViewAccessible* self,
     const FlutterSemanticsUpdate2* update) {
   g_autoptr(GHashTable) pending_children =
       g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr,
                             reinterpret_cast<GDestroyNotify>(fl_value_unref));
-
   for (size_t i = 0; i < update->node_count; i++) {
     FlutterSemanticsNode2* node = update->nodes[i];
     FlAccessibleNode* atk_node = get_node(self, node);
diff --git a/shell/platform/linux/fl_view_accessible.h b/shell/platform/linux/fl_view_accessible.h
index 0c45d7b..220338d 100644
--- a/shell/platform/linux/fl_view_accessible.h
+++ b/shell/platform/linux/fl_view_accessible.h
@@ -9,17 +9,24 @@
 #error "Only <flutter_linux/flutter_linux.h> can be included directly."
 #endif
 
-#include <gtk/gtk-a11y.h>
+#include <atk/atk.h>
 
 #include "flutter/shell/platform/embedder/embedder.h"
+#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
 
 G_BEGIN_DECLS
 
+// ATK g_autoptr macros weren't added until 2.37. Add them manually.
+// https://gitlab.gnome.org/GNOME/atk/-/issues/10
+#if !ATK_CHECK_VERSION(2, 37, 0)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(AtkPlug, g_object_unref)
+#endif
+
 G_DECLARE_FINAL_TYPE(FlViewAccessible,
                      fl_view_accessible,
                      FL,
                      VIEW_ACCESSIBLE,
-                     GtkContainerAccessible)
+                     AtkPlug)
 
 /**
  * FlViewAccessible:
@@ -29,6 +36,16 @@
  */
 
 /**
+ * fl_view_accessible_new:
+ *
+ * Creates a new accessibility object that exposes Flutter accessibility
+ * information to ATK.
+ *
+ * Returns: a new #FlViewAccessible.
+ */
+FlViewAccessible* fl_view_accessible_new(FlEngine* engine);
+
+/**
  * fl_view_accessible_handle_update_semantics:
  * @accessible: an #FlViewAccessible.
  * @update: semantic update information.
diff --git a/shell/platform/linux/fl_view_accessible_test.cc b/shell/platform/linux/fl_view_accessible_test.cc
index be4495e..0b6d17a 100644
--- a/shell/platform/linux/fl_view_accessible_test.cc
+++ b/shell/platform/linux/fl_view_accessible_test.cc
@@ -12,8 +12,7 @@
 
 TEST(FlViewAccessibleTest, BuildTree) {
   g_autoptr(FlEngine) engine = make_mock_engine();
-  g_autoptr(FlViewAccessible) accessible = FL_VIEW_ACCESSIBLE(
-      g_object_new(fl_view_accessible_get_type(), "engine", engine, nullptr));
+  g_autoptr(FlViewAccessible) accessible = fl_view_accessible_new(engine);
 
   int32_t children[] = {111, 222};
   FlutterSemanticsNode2 root_node = {
@@ -49,8 +48,7 @@
 
 TEST(FlViewAccessibleTest, AddRemoveChildren) {
   g_autoptr(FlEngine) engine = make_mock_engine();
-  g_autoptr(FlViewAccessible) accessible = FL_VIEW_ACCESSIBLE(
-      g_object_new(fl_view_accessible_get_type(), "engine", engine, nullptr));
+  g_autoptr(FlViewAccessible) accessible = fl_view_accessible_new(engine);
 
   FlutterSemanticsNode2 root_node = {
       .id = 0,