[CP-stable][tool] Fix libapp.so dropped from APK/app bundle (#188516)

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

https://github.com/flutter/flutter/issues/186810
https://github.com/flutter/flutter/issues/187388
https://github.com/flutter/flutter/issues/187553

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)?
Does it impact development (ex. flutter doctor crashes when Android Studio is installed),
or the shipping of production apps (the app crashes on launch).
This information is for domain experts and release engineers to understand the consequences of saying yes or no to the cherry pick.

The root cause either presents as the app crashing on startup, or the build failing with the message
> "Release app bundle failed to strip debug symbols from native libraries"

### Changelog Description:
Explain this cherry pick:
* In one line that is accessible to most Flutter developers.
* That describes the state prior to the fix.
* That includes which platforms are impacted.
See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples.

< Replace with changelog description here >
[flutter/186810 flutter/187388 flutter/187553] When building android app bundles using flavors, or with an old app template combined with a plugin coming alphabetically before app, fixes problems with failing to include libapp.so in the produced app bundle.

### Workaround:
Is there a workaround for this issue?

For the case of the old app template combined with a plugin coming before "app" alphabetically, there is a workaround here https://github.com/flutter/flutter/issues/186810#issuecomment-4674483652. For the flavors case there is not a workaround.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

1. make a fresh project
2. combine the subprojects in `android/build.gradle(.kts)`:
find 
```kotlin
subprojects {
    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
    project.layout.buildDirectory.value(newSubprojectBuildDir)
}

subprojects {
    project.evaluationDependsOn(":app")
}
```
and combine
```kotlin
subprojects {
    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
    project.layout.buildDirectory.value(newSubprojectBuildDir)
    project.evaluationDependsOn(":app")
}
```

3. add a plugin which comes alphabetically before "app"
4. also you will need to disable r8, due to how the templates have drifted over time.
```
// after signingConfig = signingConfigs.getByName("debug")

isMinifyEnabled = false
isShrinkResources = false
```

Also, you can separately test the repro steps listed in 
https://github.com/flutter/flutter/issues/187388
to validate that this fixes the flavors-symptom of this underlying root cause.
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
index 7d09171..df6374b 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
@@ -6,6 +6,7 @@
 
 import com.android.build.api.dsl.ApplicationExtension
 import com.android.build.api.dsl.BuildType
+import com.android.build.api.variant.AndroidComponentsExtension
 import com.android.build.gradle.AbstractAppExtension
 import com.android.build.gradle.BaseExtension
 import com.android.build.gradle.LibraryExtension
@@ -15,6 +16,7 @@
 import com.flutter.gradle.FlutterPluginConstants.PLATFORM_ABI_LIST
 import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist
 import com.flutter.gradle.plugins.PluginHandler
+import com.flutter.gradle.tasks.CopyFlutterJniLibsTask
 import com.flutter.gradle.tasks.FlutterTask
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
@@ -23,7 +25,6 @@
 import org.gradle.api.UnknownTaskException
 import org.gradle.api.file.Directory
 import org.gradle.api.tasks.Copy
-import org.gradle.api.tasks.Sync
 import org.gradle.api.tasks.TaskProvider
 import org.gradle.internal.os.OperatingSystem
 import org.gradle.kotlin.dsl.support.serviceOf
@@ -305,15 +306,35 @@
         val targetPlatforms: List<String> =
             FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo)
 
-        // TODO(reidbaker): Migrate to getAndroidApplicationExtension and getAndroidLibraryExtension.
-        val androidExtension = FlutterPluginUtils.getLegacyAndroidExtension(projectToAddTasksTo)
-        androidExtension.sourceSets.all {
-            val sourceSet = this
-            val jniLibsDir =
-                projectToAddTasksTo.layout.buildDirectory.dir(
-                    "${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${sourceSet.name}/jniLibs"
-                )
-            sourceSet.jniLibs.srcDir(jniLibsDir.get().asFile)
+        // The Android Gradle Plugin is always applied to Flutter Android projects, so its components
+        // extension is expected to be present. Use getByType (not findByType) so a misconfiguration
+        // fails loudly rather than silently skipping libapp.so registration.
+        val androidComponents = projectToAddTasksTo.extensions.getByType(AndroidComponentsExtension::class.java)
+        val targetPlatformsList = targetPlatforms
+        androidComponents.onVariants { variant ->
+            val capitalizeVariantName = FlutterPluginUtils.capitalize(variant.name)
+            // Reference the Flutter compile task by name rather than by provider: this variant API
+            // callback runs before the legacy `applicationVariants` callback in addFlutterDeps that
+            // registers the task, so its provider does not exist yet here.
+            val compileTaskName = flutterCompileTaskName(variant.name)
+            val copyJniLibsTaskProvider: TaskProvider<CopyFlutterJniLibsTask> =
+                projectToAddTasksTo.tasks.register(
+                    "copyJniLibs${FLUTTER_BUILD_PREFIX}$capitalizeVariantName",
+                    CopyFlutterJniLibsTask::class.java
+                ) {
+                    dependsOn(compileTaskName)
+                    val compileTaskProvider = projectToAddTasksTo.tasks.named(compileTaskName, FlutterTask::class.java)
+                    val outputDirProvider =
+                        compileTaskProvider.flatMap { task ->
+                            projectToAddTasksTo.layout.dir(projectToAddTasksTo.provider { task.outputDirectory!! })
+                        }
+                    intermediateDir.set(outputDirProvider)
+                    this.targetPlatforms.set(targetPlatformsList)
+                }
+            variant.sources.jniLibs?.addGeneratedSourceDirectory(
+                copyJniLibsTaskProvider,
+                CopyFlutterJniLibsTask::destinationDir
+            )
         }
 
         val flutterPlugin = this
@@ -503,6 +524,16 @@
         private const val FLUTTER_BUILD_PREFIX: String = "flutterBuild"
 
         /**
+         * The name of the [FlutterTask] (the `flutter assemble` invocation) for [variantName].
+         *
+         * Built identically by [addFlutterDeps], which registers the task, and by the variant API
+         * callback in [addFlutterTasks], which references it by name (because that callback runs
+         * before the task is registered).
+         */
+        private fun flutterCompileTaskName(variantName: String): String =
+            FlutterPluginUtils.toCamelCase(listOf("compile", FLUTTER_BUILD_PREFIX, variantName))
+
+        /**
          * Configures flutter default abi support respecting flutter command line flags.
          */
         private fun configureAbis(
@@ -660,14 +691,7 @@
 
             val variantBuildMode: String = FlutterPluginUtils.buildModeFor(variant.buildType)
             val flavorValue: String = variant.flavorName
-            val taskName: String =
-                FlutterPluginUtils.toCamelCase(
-                    listOf(
-                        "compile",
-                        FLUTTER_BUILD_PREFIX,
-                        variant.name
-                    )
-                )
+            val taskName: String = flutterCompileTaskName(variant.name)
             // The task provider below will shadow a lot of the variable names, so provide this reference
             // to access them within that scope.
 
@@ -711,39 +735,6 @@
                     flavor = flavorValue
                 }
             val flutterCompileTask: FlutterTask = compileTaskProvider.get()
-            val jniLibsDir =
-                project.layout.buildDirectory.dir(
-                    "${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/jniLibs"
-                )
-            val copyJniLibsTaskProvider: TaskProvider<Sync> =
-                project.tasks.register(
-                    "copyJniLibs${FLUTTER_BUILD_PREFIX}${FlutterPluginUtils.capitalize(variant.name)}",
-                    Sync::class.java
-                ) {
-                    dependsOn(flutterCompileTask)
-                    into(jniLibsDir)
-                    targetPlatforms.forEach { targetPlatform ->
-                        val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
-                        from("${flutterCompileTask.intermediateDir}/$abi") {
-                            include("*.so")
-                            rename { filename: String -> "lib$filename" }
-                            into(abi ?: "null")
-                        }
-                        // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble.
-                        val nativeAssetsDir =
-                            "${flutterCompileTask.intermediateDir}/native_assets/jniLibs/lib"
-                        from("$nativeAssetsDir/$abi") {
-                            include("*.so")
-                            into(abi ?: "null")
-                        }
-                    }
-                }
-            val mergeJniLibsTaskName = "merge${FlutterPluginUtils.capitalize(variant.name)}JniLibFolders"
-            project.tasks.configureEach {
-                if (name == mergeJniLibsTaskName) {
-                    dependsOn(copyJniLibsTaskProvider)
-                }
-            }
             val copyFlutterAssetsTaskProvider: TaskProvider<Copy> =
                 project.tasks.register(
                     "copyFlutterAssets${FlutterPluginUtils.capitalize(variant.name)}",
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt
new file mode 100644
index 0000000..4741fdf
--- /dev/null
+++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt
@@ -0,0 +1,66 @@
+// 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.tasks
+
+import com.flutter.gradle.FlutterPluginConstants
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import javax.inject.Inject
+
+/**
+ * Stages the native libraries produced by the Flutter build (`libapp.so` and any bundled native
+ * assets) into a dedicated [destinationDir], laid out as `<abi>/lib*.so`.
+ *
+ * It deliberately writes to its own output directory rather than into the Flutter task's output
+ * directory. Two earlier approaches dropped `libapp.so` from the APK/app bundle: nesting this output
+ * inside the Flutter task's output directory created overlapping task outputs that broke Gradle's
+ * incremental checks (flavored single-ABI rebuilds), and registering the staged directory eagerly as
+ * a source set `srcDir` captured the build directory before it had been redirected. See
+ * https://github.com/flutter/flutter/issues/186810 and
+ * https://github.com/flutter/flutter/issues/187388.
+ */
+abstract class CopyFlutterJniLibsTask : DefaultTask() {
+    /** The Flutter build output directory (the `flutter assemble` `--output` location). */
+    @get:InputDirectory
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val intermediateDir: DirectoryProperty
+
+    @get:Input
+    abstract val targetPlatforms: ListProperty<String>
+
+    @get:OutputDirectory
+    abstract val destinationDir: DirectoryProperty
+
+    @get:Inject
+    abstract val fileSystemOperations: FileSystemOperations
+
+    @TaskAction
+    fun copy() {
+        fileSystemOperations.sync {
+            into(destinationDir)
+            targetPlatforms.get().forEach { targetPlatform ->
+                val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
+                from(intermediateDir.dir(abi ?: "null")) {
+                    include("*.so")
+                    rename { filename: String -> "lib$filename" }
+                    into(abi ?: "null")
+                }
+                val nativeAssetsDir = intermediateDir.dir("native_assets/jniLibs/lib/$abi")
+                from(nativeAssetsDir) {
+                    include("*.so")
+                    into(abi ?: "null")
+                }
+            }
+        }
+    }
+}
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
index c1cbdce..38b916f 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
@@ -59,10 +59,11 @@
         every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
         val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
         every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
-        every { mockAndroidComponentsExtension.selector() } returns
-            mockk {
-                every { all() } returns mockk()
-            }
+        every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
+        val mockSelector = mockk<com.android.build.api.variant.VariantSelector>(relaxed = true)
+        every { mockAndroidComponentsExtension.selector() } returns mockSelector
+        every { mockSelector.all() } returns mockSelector
+        every { mockSelector.withName(any<String>()) } returns mockSelector
         every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension
         every { project.extensions.getByType(LibraryExtension::class.java) } returns mockLibraryExtension
         every { project.extensions.findByName("android") } returns mockAbstractAppExtension
@@ -151,10 +152,11 @@
         every { project.extensions.findByName("android") } returns mockAbstractAppExtension
         val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>(relaxed = true)
         every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
-        every { mockAndroidComponentsExtension.selector() } returns
-            mockk {
-                every { all() } returns mockk()
-            }
+        every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension
+        val mockSelector = mockk<com.android.build.api.variant.VariantSelector>(relaxed = true)
+        every { mockAndroidComponentsExtension.selector() } returns mockSelector
+        every { mockSelector.all() } returns mockSelector
+        every { mockSelector.withName(any<String>()) } returns mockSelector
         every { project.projectDir } returns projectDir.toFile()
         every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString()
         every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile()
diff --git a/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart b/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart
new file mode 100644
index 0000000..f76be44
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart
@@ -0,0 +1,423 @@
+// 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.
+
+// Regression tests for `libapp.so` being dropped from the APK/app bundle, which
+// surfaces either as a runtime "VM snapshot invalid" crash (APK) or as a
+// "Release app bundle failed to strip debug symbols from native libraries"
+// build failure (app bundle).
+//
+// Root cause: https://github.com/flutter/flutter/issues/186810 and
+// https://github.com/flutter/flutter/issues/187388 (a regression from
+// https://github.com/flutter/flutter/pull/181275, which moved `libapp.so` from
+// a jar dependency onto a Flutter Gradle Plugin source-set `jniLibs` directory).
+//
+// These tests cover the two confirmed triggers:
+//   * Case A: a combined `subprojects { ... evaluationDependsOn(":app") }` block
+//     in the root `android/build.gradle.kts` together with a plugin whose Gradle
+//     subproject name sorts alphabetically before `:app`. This evaluates `:app`
+//     before its build directory is redirected.
+//   * Case B: a flavored project where a build for a single ABI (e.g. a prior
+//     `flutter run` on one device) leaves stale incremental state that drops
+//     `libapp.so` for the other ABIs on the next multi-ABI build.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:archive/archive.dart';
+import 'package:file/file.dart';
+import 'package:file_testing/file_testing.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import '../src/common.dart';
+import 'test_utils.dart';
+
+void main() {
+  late Directory tempDir;
+
+  setUp(() {
+    tempDir = createResolvedTempDirectorySync('flutter_libapp_so_test.');
+  });
+
+  tearDown(() {
+    tryToDelete(tempDir);
+  });
+
+  String getTestFileString(String probe) {
+    return '''
+         import 'package:flutter/material.dart';
+
+         void main() {
+           print('$probe');
+           runApp(const MyApp());
+         }
+
+         class MyApp extends StatelessWidget {
+           const MyApp({super.key});
+
+           @override
+           Widget build(BuildContext context) {
+             return const MaterialApp(
+               home: Scaffold(
+                 body: Center(
+                   child: Text('Hello World'),
+                 ),
+               ),
+             );
+           }
+         }
+         ''';
+  }
+
+  ArchiveFile? getLibAppFile(File apkFile) {
+    final List<int> apkBytes = apkFile.readAsBytesSync();
+    final Archive archive = ZipDecoder().decodeBytes(apkBytes);
+    return archive.findFile('lib/arm64-v8a/libapp.so') ??
+        archive.findFile('lib/armeabi-v7a/libapp.so') ??
+        archive.findFile('lib/x86_64/libapp.so');
+  }
+
+  test('libapp.so is packaged in an app bundle with a combined subprojects block '
+      'and a plugin sorted before ":app"', () async {
+    final Directory appDir = _createApp(tempDir);
+
+    // A plugin whose Gradle subproject (":aaa_plugin") sorts before ":app", so
+    // the combined subprojects loop reaches it (and triggers
+    // evaluationDependsOn(":app")) before ":app"'s build dir is redirected.
+    _createPlugin(tempDir, 'aaa_plugin');
+    _addPathDependency(appDir, 'aaa_plugin', tempDir.childDirectory('aaa_plugin'));
+
+    _useCombinedSubprojectsBlock(appDir);
+    _disableR8(appDir);
+
+    final ProcessResult result = processManager.runSync(<String>[
+      flutterBin,
+      'build',
+      'appbundle',
+      '--release',
+    ], workingDirectory: appDir.path);
+    expect(
+      result.exitCode,
+      0,
+      reason: 'flutter build appbundle --release failed:\n${result.stdout}\n${result.stderr}',
+    );
+
+    final List<String> files = _appBundleFileList(appDir, _releaseBundle(appDir));
+    for (final arch in <String>['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
+      expect(
+        files,
+        contains('base/lib/$arch/libapp.so'),
+        reason: 'libapp.so missing for $arch in the app bundle',
+      );
+    }
+  });
+
+  test('libapp.so is packaged for every ABI after a single-ABI build of a flavored app', () async {
+    final Directory appDir = _createApp(tempDir);
+    _addFlavors(appDir, <String>['prod']);
+    _disableR8(appDir);
+
+    // Simulate a prior `flutter run` on a single-architecture device, which only
+    // builds `app.so` for that ABI.
+    final ProcessResult singleAbiBuild = processManager.runSync(<String>[
+      flutterBin,
+      'build',
+      'apk',
+      '--release',
+      '--flavor',
+      'prod',
+      '--target-platform',
+      'android-arm64',
+    ], workingDirectory: appDir.path);
+    expect(
+      singleAbiBuild.exitCode,
+      0,
+      reason: 'single-ABI build failed:\n${singleAbiBuild.stdout}\n${singleAbiBuild.stderr}',
+    );
+
+    // Now build for all ABIs. `libapp.so` must be present for all of them, not
+    // just the one built above.
+    final ProcessResult allAbiBuild = processManager.runSync(<String>[
+      flutterBin,
+      'build',
+      'appbundle',
+      '--release',
+      '--flavor',
+      'prod',
+    ], workingDirectory: appDir.path);
+    expect(
+      allAbiBuild.exitCode,
+      0,
+      reason: 'multi-ABI build failed:\n${allAbiBuild.stdout}\n${allAbiBuild.stderr}',
+    );
+
+    final List<String> files = _appBundleFileList(
+      appDir,
+      appDir
+          .childDirectory('build')
+          .childDirectory('app')
+          .childDirectory('outputs')
+          .childDirectory('bundle')
+          .childDirectory('prodRelease')
+          .childFile('app-prod-release.aab'),
+    );
+    for (final arch in <String>['arm64-v8a', 'armeabi-v7a', 'x86_64']) {
+      expect(
+        files,
+        contains('base/lib/$arch/libapp.so'),
+        reason: 'libapp.so missing for $arch after a single-ABI build preceded the multi-ABI build',
+      );
+    }
+  });
+
+  // Reproduction test for https://github.com/flutter/flutter/issues/187553
+  testWithoutContext('mergeJniLibFolders runs when Dart AOT source changes with flavors', () async {
+    final Directory projectDir = tempDir.childDirectory('app');
+
+    // 1. Create a Flutter app template.
+    final ProcessResult createResult = processManager.runSync(<String>[
+      flutterBin,
+      'create',
+      '--template=app',
+      '--platforms=android',
+      'app',
+    ], workingDirectory: tempDir.path);
+    expect(createResult, const ProcessResultMatcher());
+
+    // 2. Add product flavors to build.gradle.kts.
+    final File buildGradleFile = projectDir
+        .childDirectory('android/app')
+        .childFile('build.gradle.kts');
+    expect(buildGradleFile, exists);
+    String buildGradleContents = buildGradleFile.readAsStringSync();
+    buildGradleContents = buildGradleContents.replaceFirst('android {', '''
+android {
+    flavorDimensions += "default"
+    productFlavors {
+        create("prod") {
+            dimension = "default"
+        }
+        create("dev") {
+            dimension = "default"
+        }
+    }''');
+    buildGradleFile.writeAsStringSync(buildGradleContents);
+
+    const PROBE_A = 'probe-AAAA';
+    const PROBE_B = 'probe-BBBB';
+    // 3. Configure main.dart with probe-AAAA.
+    final File mainDartFile = projectDir.childDirectory('lib').childFile('main.dart');
+    expect(mainDartFile, exists);
+    mainDartFile.writeAsStringSync(getTestFileString(PROBE_A));
+
+    // 4. Build APK with flavor prod (Build 1).
+    final ProcessResult build1Result = processManager.runSync(<String>[
+      flutterBin,
+      'build',
+      'apk',
+      '--release',
+      '--flavor',
+      'prod',
+    ], workingDirectory: projectDir.path);
+    expect(build1Result, const ProcessResultMatcher());
+
+    // Verify APK contains probe-AAAA.
+    final File apkFile = projectDir
+        .childDirectory('build/app/outputs/flutter-apk')
+        .childFile('app-prod-release.apk');
+    expect(apkFile, exists);
+
+    ArchiveFile? libappFile = getLibAppFile(apkFile);
+    expect(libappFile, isNotNull);
+    String libappContent = latin1.decode(libappFile!.content as List<int>, allowInvalid: true);
+    expect(libappContent.contains(PROBE_A), isTrue);
+    expect(libappContent.contains(PROBE_B), isFalse);
+
+    // 5. Change main.dart to probe-BBBB.
+    mainDartFile.writeAsStringSync(getTestFileString(PROBE_B));
+
+    // 6. Build APK with flavor prod (Build 2 - incremental).
+    final ProcessResult build2Result = processManager.runSync(<String>[
+      flutterBin,
+      'build',
+      'apk',
+      '--release',
+      '--flavor',
+      'prod',
+    ], workingDirectory: projectDir.path);
+    expect(build2Result, const ProcessResultMatcher());
+
+    // Verify APK contains probe-BBBB and NOT probe-AAAA.
+    libappFile = getLibAppFile(apkFile);
+    expect(libappFile, isNotNull);
+    libappContent = latin1.decode(libappFile!.content as List<int>, allowInvalid: true);
+    expect(libappContent.contains('probe-BBBB'), isTrue);
+    expect(libappContent.contains('probe-AAAA'), isFalse);
+  });
+}
+
+Directory _createApp(Directory workingDir) {
+  final ProcessResult result = processManager.runSync(<String>[
+    flutterBin,
+    'create',
+    '--template=app',
+    '--platforms=android',
+    'app',
+  ], workingDirectory: workingDir.path);
+  if (result.exitCode != 0) {
+    throw StateError('flutter create app failed:\n${result.stdout}\n${result.stderr}');
+  }
+  return workingDir.childDirectory('app');
+}
+
+void _createPlugin(Directory workingDir, String name) {
+  final ProcessResult result = processManager.runSync(<String>[
+    flutterBin,
+    'create',
+    '--template=plugin',
+    '--platforms=android',
+    name,
+  ], workingDirectory: workingDir.path);
+  if (result.exitCode != 0) {
+    throw StateError('flutter create plugin failed:\n${result.stdout}\n${result.stderr}');
+  }
+}
+
+void _addPathDependency(Directory appDir, String name, Directory packageDir) {
+  final File pubspec = appDir.childFile('pubspec.yaml');
+  final String contents = pubspec.readAsStringSync();
+  // Insert the path dependency immediately under the `dependencies:` key.
+  final String updated = contents.replaceFirst(
+    RegExp(r'^dependencies:\s*$', multiLine: true),
+    'dependencies:\n  $name:\n    path: ${packageDir.path.replaceAll(r'\', '/')}',
+  );
+  if (updated == contents) {
+    throw StateError('Failed to add path dependency to ${pubspec.path}');
+  }
+  pubspec.writeAsStringSync(updated);
+}
+
+/// Rewrites the two separate `subprojects { ... }` blocks generated by the app
+/// template into the single combined block that triggers the regression.
+void _useCombinedSubprojectsBlock(Directory appDir) {
+  final File buildGradle = appDir.childDirectory('android').childFile('build.gradle.kts');
+  final String contents = buildGradle.readAsStringSync().replaceAll('\r\n', '\n');
+  const separateBlocks = '''
+subprojects {
+    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+    project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+    project.evaluationDependsOn(":app")
+}''';
+  const combinedBlock = '''
+subprojects {
+    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+    project.layout.buildDirectory.value(newSubprojectBuildDir)
+    project.evaluationDependsOn(":app")
+}''';
+  if (!contents.contains(separateBlocks)) {
+    throw StateError(
+      'Could not find the expected separate subprojects blocks in ${buildGradle.path}.\n'
+      'Template may have changed; update this test.',
+    );
+  }
+  buildGradle.writeAsStringSync(contents.replaceFirst(separateBlocks, combinedBlock));
+}
+
+void _addFlavors(Directory appDir, List<String> flavors) {
+  final File buildGradle = appDir
+      .childDirectory('android')
+      .childDirectory('app')
+      .childFile('build.gradle.kts');
+  final String contents = buildGradle.readAsStringSync();
+  final buffer = StringBuffer()
+    ..writeln('    flavorDimensions += "env"')
+    ..writeln('    productFlavors {');
+  for (final flavor in flavors) {
+    buffer
+      ..writeln('        create("$flavor") {')
+      ..writeln('            dimension = "env"')
+      ..writeln('        }');
+  }
+  buffer.writeln('    }');
+  // Insert the product flavors at the start of the `android { ... }` block.
+  final String updated = contents.replaceFirst(
+    RegExp(r'^android\s*\{', multiLine: true),
+    'android {\n$buffer',
+  );
+  if (updated == contents) {
+    throw StateError('Failed to add product flavors to ${buildGradle.path}');
+  }
+  buildGradle.writeAsStringSync(updated);
+}
+
+void _disableR8(Directory appDir) {
+  final File buildGradle = appDir
+      .childDirectory('android')
+      .childDirectory('app')
+      .childFile('build.gradle.kts');
+  final String contents = buildGradle.readAsStringSync();
+  final String updated = contents.replaceFirst(
+    'signingConfig = signingConfigs.getByName("debug")',
+    'signingConfig = signingConfigs.getByName("debug")\n            isMinifyEnabled = false\n            isShrinkResources = false',
+  );
+  if (updated == contents) {
+    throw StateError('Failed to disable R8 in ${buildGradle.path}');
+  }
+  buildGradle.writeAsStringSync(updated);
+}
+
+File _releaseBundle(Directory appDir) => appDir
+    .childDirectory('build')
+    .childDirectory('app')
+    .childDirectory('outputs')
+    .childDirectory('bundle')
+    .childDirectory('release')
+    .childFile('app-release.aab');
+
+/// Returns the file entries inside [appBundle] using `apkanalyzer files list`.
+List<String> _appBundleFileList(Directory appDir, File appBundle) {
+  if (!appBundle.existsSync()) {
+    throw StateError('App bundle not found at ${appBundle.path}');
+  }
+  final File localProperties = appDir.childDirectory('android').childFile('local.properties');
+  final RegExpMatch? match = RegExp(
+    r'sdk\.dir=(.+)',
+  ).firstMatch(localProperties.readAsStringSync());
+  final String sdkPath = match?.group(1)?.trim() ?? '';
+  if (sdkPath.isEmpty) {
+    throw StateError('SDK path not found in ${localProperties.path}');
+  }
+  final String apkAnalyzer = fileSystem
+      .directory(sdkPath)
+      .childDirectory('cmdline-tools')
+      .childDirectory('latest')
+      .childDirectory('bin')
+      .childFile(Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer')
+      .path;
+
+  final ProcessResult result = processManager.runSync(<String>[
+    apkAnalyzer,
+    'files',
+    'list',
+    appBundle.path,
+  ]);
+  if (result.exitCode != 0) {
+    throw ProcessException(
+      apkAnalyzer,
+      <String>['files', 'list', appBundle.path],
+      'apkanalyzer failed:\n${result.stderr}',
+      result.exitCode,
+    );
+  }
+  // apkanalyzer prints entries like `/base/lib/arm64-v8a/libapp.so`; normalize
+  // by trimming the leading slash.
+  return result.stdout
+      .toString()
+      .split('\n')
+      .map((String line) => line.trim())
+      .where((String line) => line.isNotEmpty)
+      .map((String line) => line.startsWith('/') ? line.substring(1) : line)
+      .toList();
+}