Fix the application not disposing by breaking circular references on quit (#47684)

GtkApplication windows contain a reference back to the application. This
means the MyApplication object in the Flutter application never
disposes. We workaround this by removing these references before we
quit. This occurs for all GTK applications that quit using this method,
see https://gitlab.gnome.org/GNOME/gtk/-/issues/6190. This may be fixed
upstream at some point but the proposed workaround should solve the
problem for all versions and not conflict with any upstream solution.

Fixes https://github.com/flutter/flutter/issues/136582
diff --git a/shell/platform/linux/fl_platform_plugin.cc b/shell/platform/linux/fl_platform_plugin.cc
index 008d3d0..2f4beb4 100644
--- a/shell/platform/linux/fl_platform_plugin.cc
+++ b/shell/platform/linux/fl_platform_plugin.cc
@@ -186,9 +186,20 @@
   if (app == nullptr) {
     // Unable to gracefully quit, so just exit the process.
     exit(0);
-  } else {
-    g_application_quit(app);
   }
+
+  // GtkApplication windows contain a reference back to the application.
+  // Break them so the application object can cleanup.
+  // See https://gitlab.gnome.org/GNOME/gtk/-/issues/6190
+  if (GTK_IS_APPLICATION(app)) {
+    GList* windows = gtk_application_get_windows(GTK_APPLICATION(app));
+    for (GList* link = windows; link != NULL; link = link->next) {
+      GtkWidget* window = GTK_WIDGET(link->data);
+      gtk_window_set_application(GTK_WINDOW(window), NULL);
+    }
+  }
+
+  g_application_quit(app);
 }
 
 // Handle response of System.requestAppExit.
diff --git a/shell/platform/linux/fl_platform_plugin_test.cc b/shell/platform/linux/fl_platform_plugin_test.cc
index 09b2b7f..e5fe1d7 100644
--- a/shell/platform/linux/fl_platform_plugin_test.cc
+++ b/shell/platform/linux/fl_platform_plugin_test.cc
@@ -2,9 +2,11 @@
 // 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_platform_plugin.h"
+#include <gtk/gtk.h>
+
 #include "flutter/shell/platform/linux/fl_binary_messenger_private.h"
 #include "flutter/shell/platform/linux/fl_method_codec_private.h"
+#include "flutter/shell/platform/linux/fl_platform_plugin.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_method_codec.h"
 #include "flutter/shell/platform/linux/testing/fl_test.h"
@@ -87,6 +89,87 @@
   return fl_value_equal(arg, value);
 }
 
+G_DECLARE_FINAL_TYPE(FlTestApplication,
+                     fl_test_application,
+                     FL,
+                     TEST_APPLICATION,
+                     GtkApplication)
+
+struct _FlTestApplication {
+  GtkApplication parent_instance;
+  gboolean* dispose_called;
+};
+
+G_DEFINE_TYPE(FlTestApplication,
+              fl_test_application,
+              gtk_application_get_type())
+
+static void fl_test_application_startup(GApplication* application) {
+  G_APPLICATION_CLASS(fl_test_application_parent_class)->startup(application);
+
+  // Add a window to this application, which will hold a reference to the
+  // application and stop it disposing. See
+  // https://gitlab.gnome.org/GNOME/gtk/-/issues/6190
+  gtk_application_window_new(GTK_APPLICATION(application));
+}
+
+static void fl_test_application_activate(GApplication* application) {
+  G_APPLICATION_CLASS(fl_test_application_parent_class)->activate(application);
+
+  ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
+  g_autoptr(FlPlatformPlugin) plugin = fl_platform_plugin_new(messenger);
+  EXPECT_NE(plugin, nullptr);
+  g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
+
+  g_autoptr(FlValue) exit_result = fl_value_new_map();
+  fl_value_set_string_take(exit_result, "response",
+                           fl_value_new_string("exit"));
+  EXPECT_CALL(messenger,
+              fl_binary_messenger_send_response(
+                  ::testing::Eq<FlBinaryMessenger*>(messenger), ::testing::_,
+                  SuccessResponse(exit_result), ::testing::_))
+      .WillOnce(::testing::Return(true));
+
+  // Request app exit.
+  g_autoptr(FlValue) args = fl_value_new_map();
+  fl_value_set_string_take(args, "type", fl_value_new_string("required"));
+  g_autoptr(GBytes) message = fl_method_codec_encode_method_call(
+      FL_METHOD_CODEC(codec), "System.exitApplication", args, nullptr);
+  messenger.ReceiveMessage("flutter/platform", message);
+}
+
+static void fl_test_application_dispose(GObject* object) {
+  FlTestApplication* self = FL_TEST_APPLICATION(object);
+
+  *self->dispose_called = true;
+
+  G_OBJECT_CLASS(fl_test_application_parent_class)->dispose(object);
+}
+
+static void fl_test_application_class_init(FlTestApplicationClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_test_application_dispose;
+  G_APPLICATION_CLASS(klass)->startup = fl_test_application_startup;
+  G_APPLICATION_CLASS(klass)->activate = fl_test_application_activate;
+}
+
+static void fl_test_application_init(FlTestApplication* self) {}
+
+FlTestApplication* fl_test_application_new(gboolean* dispose_called) {
+  FlTestApplication* self = FL_TEST_APPLICATION(
+      g_object_new(fl_test_application_get_type(), nullptr));
+
+  // Don't try and register on D-Bus.
+  g_application_set_application_id(G_APPLICATION(self), "dev.flutter.GtkTest");
+  g_application_set_flags(G_APPLICATION(self), G_APPLICATION_NON_UNIQUE);
+
+  // Added to stop compiler complaining about an unused function.
+  FL_IS_TEST_APPLICATION(self);
+
+  self->dispose_called = dispose_called;
+
+  return self;
+}
+
 TEST(FlPlatformPluginTest, PlaySound) {
   ::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
 
@@ -142,3 +225,17 @@
       FL_METHOD_CODEC(codec), "System.exitApplication", args, nullptr);
   messenger.ReceiveMessage("flutter/platform", message);
 }
+
+TEST(FlPlatformPluginTest, ExitApplicationDispose) {
+  gtk_init(0, nullptr);
+
+  gboolean dispose_called = false;
+  FlTestApplication* application = fl_test_application_new(&dispose_called);
+
+  // Run the application, it will quit after startup.
+  g_application_run(G_APPLICATION(application), 0, nullptr);
+
+  EXPECT_FALSE(dispose_called);
+  g_object_unref(application);
+  EXPECT_TRUE(dispose_called);
+}