Add our own wrapper for `CommonExtension` due to change in signature from 8.x->9.0 (#184433)

We can't reference this type directly because of:

https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/CommonExtension

https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/CommonExtension

---------

Co-authored-by: Gray Mackall <mackall@google.com>
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt b/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt
new file mode 100644
index 0000000..db0943a
--- /dev/null
+++ b/packages/flutter_tools/gradle/src/main/kotlin/AgpCommonExtensionWrapper.kt
@@ -0,0 +1,111 @@
+// Copyright 2014 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.
+
+package com.flutter.gradle
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.BuildType
+import com.android.build.api.dsl.DynamicFeatureExtension
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.dsl.TestExtension
+import org.gradle.api.NamedDomainObjectContainer
+import java.io.File
+
+/**
+ * A wrapper to bypass binary incompatibilities in AGP's CommonExtension between 8.x and 9.x.
+ * * CRITICAL: Do not import or reference `com.android.build.api.dsl.CommonExtension`
+ * anywhere in this file, or the compiler may weave the broken type into the bytecode.
+ */
+class AgpCommonExtensionWrapper(
+    private val backingExtension: Any
+) {
+    var compileSdk: Int?
+        get() =
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.compileSdk
+                is LibraryExtension -> backingExtension.compileSdk
+                is DynamicFeatureExtension -> backingExtension.compileSdk
+                is TestExtension -> backingExtension.compileSdk
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        set(value) {
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.compileSdk = value
+                is LibraryExtension -> backingExtension.compileSdk = value
+                is DynamicFeatureExtension -> backingExtension.compileSdk = value
+                is TestExtension -> backingExtension.compileSdk = value
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        }
+
+    var namespace: String?
+        get() =
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.namespace
+                is LibraryExtension -> backingExtension.namespace
+                is DynamicFeatureExtension -> backingExtension.namespace
+                is TestExtension -> backingExtension.namespace
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        set(value) {
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.namespace = value
+                is LibraryExtension -> backingExtension.namespace = value
+                is DynamicFeatureExtension -> backingExtension.namespace = value
+                is TestExtension -> backingExtension.namespace = value
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        }
+
+    var ndkVersion: String
+        get() =
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.ndkVersion
+                is LibraryExtension -> backingExtension.ndkVersion
+                is DynamicFeatureExtension -> backingExtension.ndkVersion
+                is TestExtension -> backingExtension.ndkVersion
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        set(value) {
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.ndkVersion = value
+                is LibraryExtension -> backingExtension.ndkVersion = value
+                is DynamicFeatureExtension -> backingExtension.ndkVersion = value
+                is TestExtension -> backingExtension.ndkVersion = value
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+        }
+
+    val buildTypes: NamedDomainObjectContainer<out BuildType>
+        get() =
+            when (backingExtension) {
+                is ApplicationExtension -> backingExtension.buildTypes
+                is LibraryExtension -> backingExtension.buildTypes
+                is DynamicFeatureExtension -> backingExtension.buildTypes
+                is TestExtension -> backingExtension.buildTypes
+                else -> throw IllegalArgumentException(unsupportedMessage())
+            }
+
+    fun getDefaultProguardFile(fileName: String): File =
+        when (backingExtension) {
+            is ApplicationExtension -> backingExtension.getDefaultProguardFile(fileName)
+            is LibraryExtension -> backingExtension.getDefaultProguardFile(fileName)
+            is DynamicFeatureExtension -> backingExtension.getDefaultProguardFile(fileName)
+            is TestExtension -> backingExtension.getDefaultProguardFile(fileName)
+            else -> throw IllegalArgumentException(unsupportedMessage())
+        }
+
+    // Example of wrapping a method rather than a property
+    fun compileOptions(action: (Any) -> Unit) {
+        when (backingExtension) {
+            is ApplicationExtension -> backingExtension.compileOptions { action(this) }
+            is LibraryExtension -> backingExtension.compileOptions { action(this) }
+            is DynamicFeatureExtension -> backingExtension.compileOptions { action(this) }
+            is TestExtension -> backingExtension.compileOptions { action(this) }
+            else -> throw IllegalArgumentException(unsupportedMessage())
+        }
+    }
+
+    private fun unsupportedMessage() = "Unsupported Android extension type: ${backingExtension.javaClass.name}"
+}
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
index 660c6a5..eba58ee 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
@@ -6,7 +6,6 @@
 
 import com.android.build.api.artifact.SingleArtifact
 import com.android.build.api.dsl.ApplicationExtension
-import com.android.build.api.dsl.CommonExtension
 import com.android.build.api.dsl.LibraryExtension
 import com.android.build.api.variant.AndroidComponentsExtension
 import com.android.build.gradle.BaseExtension
@@ -409,8 +408,14 @@
         return project.extensions.findByType(BaseExtension::class.java)!!
     }
 
-    internal fun getAndroidExtension(project: Project): CommonExtension<*, *, *, *, *, *> =
-        project.extensions.findByType(CommonExtension::class.java)!!
+    internal fun getAndroidExtension(project: Project): AgpCommonExtensionWrapper {
+        // Look up by name to completely avoid importing or resolving CommonExtension
+        val androidExtension =
+            project.extensions.findByName("android")
+                ?: throw IllegalStateException("The Android plugin must be applied before accessing the Android extension.")
+
+        return AgpCommonExtensionWrapper(androidExtension)
+    }
 
     internal fun getAndroidLibraryExtension(project: Project): LibraryExtension = project.extensions.getByType(LibraryExtension::class.java)
 
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
index a182517..c1cbdce 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
@@ -1,5 +1,6 @@
 package com.flutter.gradle
 
+import com.android.build.api.dsl.ApplicationBuildType
 import com.android.build.api.dsl.ApplicationDefaultConfig
 import com.android.build.api.dsl.ApplicationExtension
 import com.android.build.api.dsl.CommonExtension
@@ -49,7 +50,11 @@
         val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
         fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM)
         val project = mockk<Project>(relaxed = true)
-        val mockAbstractAppExtension = mockk<AbstractAppExtension>(relaxed = true)
+        val mockAbstractAppExtension =
+            mockk<AbstractAppExtension>(
+                moreInterfaces = arrayOf(ApplicationExtension::class),
+                relaxed = true
+            )
         val mockLibraryExtension = mockk<LibraryExtension>(relaxed = true)
         every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
         val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
@@ -69,19 +74,35 @@
         every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
         val mockBaseExtension = mockk<BaseExtension>(relaxed = true)
         val mockCommonExtension = mockk<CommonExtension<*, *, *, *, *, *>>(relaxed = true)
-        val mockDebugBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
-        val mockReleaseBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
+        val mockDebugBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+        val mockReleaseBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+
+        // Cast our multi-interface mock instead of creating a brand new one
+        val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension
+
+        // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them
+        every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
+        every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
+        // Keep the CommonExtension mocks just in case other parts of the plugin look for it
         every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
         every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
         every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension
         every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension
-        val mockApplicationExtension = mockk<ApplicationExtension>(relaxed = true)
+
+        // Pass the dual-purpose mock for any ApplicationExtension lookups
         every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
         every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension
-        val mockApplicationDefaultConfig = mockk<ApplicationDefaultConfig>(relaxed = true)
+
+        val mockApplicationDefaultConfig =
+            mockk<com.android.build.gradle.internal.dsl.DefaultConfig>(
+                moreInterfaces = arrayOf(ApplicationDefaultConfig::class),
+                relaxed = true
+            )
         every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
         every { project.rootProject } returns project
-        every { project.state.failure } returns null
+        every { project.state.failure as Throwable? } returns null
         val mockDirectory = mockk<Directory>(relaxed = true)
         every { project.layout.buildDirectory.get() } returns mockDirectory
         val mockAndroidSourceSet = mockk<com.android.build.gradle.api.AndroidSourceSet>(relaxed = true)
@@ -120,7 +141,11 @@
         val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm")
         fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM)
         val project = mockk<Project>(relaxed = true)
-        val mockAbstractAppExtension = mockk<AbstractAppExtension>(relaxed = true)
+        val mockAbstractAppExtension =
+            mockk<AbstractAppExtension>(
+                moreInterfaces = arrayOf(ApplicationExtension::class),
+                relaxed = true
+            )
         every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
         every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
         every { project.extensions.findByName("android") } returns mockAbstractAppExtension
@@ -138,19 +163,35 @@
         every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension
         val mockBaseExtension = mockk<BaseExtension>(relaxed = true)
         val mockCommonExtension = mockk<CommonExtension<*, *, *, *, *, *>>(relaxed = true)
-        val mockDebugBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
-        val mockReleaseBuildType = mockk<com.android.build.api.dsl.BuildType>(relaxed = true)
+        val mockDebugBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+        val mockReleaseBuildType = mockk<com.android.build.api.dsl.ApplicationBuildType>(relaxed = true)
+
+        // Cast our multi-interface mock instead of creating a brand new one
+        val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension
+
+        // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them
+        every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
+        every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
+        // Keep the CommonExtension mocks just in case other parts of the plugin look for it
         every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType
         every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType
+
         every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension
         every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension
-        val mockApplicationExtension = mockk<ApplicationExtension>(relaxed = true)
+
+        // Pass the dual-purpose mock for any ApplicationExtension lookups
         every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension
         every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension
-        val mockApplicationDefaultConfig = mockk<ApplicationDefaultConfig>(relaxed = true)
+
+        val mockApplicationDefaultConfig =
+            mockk<com.android.build.gradle.internal.dsl.DefaultConfig>(
+                moreInterfaces = arrayOf(ApplicationDefaultConfig::class),
+                relaxed = true
+            )
         every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig
         every { project.rootProject } returns project
-        every { project.state.failure } returns null
+        every { project.state.failure as Throwable? } returns null
         val mockDirectory = mockk<Directory>(relaxed = true)
         every { project.layout.buildDirectory.get() } returns mockDirectory
         val mockAndroidSourceSet = mockk<com.android.build.gradle.api.AndroidSourceSet>(relaxed = true)