More `FlutterPlugin` static method conversion (#165506)

Moves a lot more functionality from `flutter.groovy` to
`FlutterPluginUtils.kt` that could be made static.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Gray Mackall <mackall@google.com>
diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
index 2f3fb00..7d96fcb 100644
--- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
+++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
@@ -5,6 +5,7 @@
 
 import com.android.build.OutputFile
 import com.android.build.gradle.AbstractAppExtension
+import com.android.tools.r8.P
 import com.flutter.gradle.AppLinkSettings
 import com.android.build.gradle.api.BaseVariantOutput
 import com.android.build.gradle.tasks.PackageAndroidArtifact
@@ -72,7 +73,7 @@
         this.project = project
 
         Project rootProject = project.rootProject
-        if (isFlutterAppProject()) {
+        if (FlutterPluginUtils.isFlutterAppProject(project)) {
             rootProject.tasks.register("generateLockfiles") {
                 doLast {
                     rootProject.subprojects.each { subproject ->
@@ -135,7 +136,7 @@
         extension.flutterVersionName = localProperties.getProperty("flutter.versionName", "1.0")
 
         this.addFlutterTasks(project)
-        forceNdkDownload(project, flutterRootPath)
+        FlutterPluginUtils.forceNdkDownload(project, flutterRootPath)
 
         // By default, assembling APKs generates fat APKs if multiple platforms are passed.
         // Configuring split per ABI allows to generate separate APKs for each abi.
@@ -162,7 +163,7 @@
             }
         }
 
-        getTargetPlatforms().each { targetArch ->
+        FlutterPluginUtils.getTargetPlatforms(project).each { targetArch ->
             String abiValue = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetArch]
             project.android {
                 if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
@@ -256,52 +257,6 @@
         project.android.buildTypes.all(this.&addFlutterDependencies)
     }
 
-    private static Properties readPropertiesIfExist(File propertiesFile) {
-        Properties result = new Properties()
-        if (propertiesFile.exists()) {
-            propertiesFile.withReader("UTF-8") { reader -> result.load(reader) }
-        }
-        return result
-    }
-
-    // Add a task that can be called on flutter projects that prints the Java version used in Gradle.
-    //
-    // Format of the output of this task can be used in debugging what version of Java Gradle is using.
-    // Not recommended for use in time sensitive commands like `flutter run` or `flutter build` as
-    // Gradle is slower than we want. Particularly in light of https://github.com/flutter/flutter/issues/119196.
-    private static void addTaskForJavaVersion(Project project) {
-        // Warning: the name of this task is used by other code. Change with caution.
-        project.tasks.register("javaVersion") {
-            description "Print the current java version used by gradle. "
-                "see: https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
-            doLast {
-                println(JavaVersion.current())
-            }
-        }
-    }
-
-    // Add a task that can be called on Flutter projects that prints the available build variants
-    // in Gradle.
-    //
-    // This task prints variants in this format:
-    //
-    // BuildVariant: debug
-    // BuildVariant: release
-    // BuildVariant: profile
-    //
-    // Format of the output of this task is used by `AndroidProject.getBuildVariants`.
-    private static void addTaskForPrintBuildVariants(Project project) {
-        // Warning: The name of this task is used by `AndroidProject.getBuildVariants`.
-        project.tasks.register("printBuildVariants") {
-            description "Prints out all build variants for this Android project"
-            doLast {
-                project.android.applicationVariants.all { variant ->
-                    println "BuildVariant: ${variant.name}"
-                }
-            }
-        }
-    }
-
     // Add a task that can be called on Flutter projects that outputs app link related project
     // settings into a json file.
     //
@@ -428,29 +383,7 @@
      *    2. libflutter.so
      */
     void addFlutterDependencies(BuildType buildType) {
-        String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
-        if (!FlutterPluginUtils.supportsBuildMode(project, flutterBuildMode)) {
-            return
-        }
-        // The embedding is set as an API dependency in a Flutter plugin.
-        // Therefore, don't make the app project depend on the embedding if there are Flutter
-        // plugin dependencies. In release mode, dev dependencies are stripped, so we do not
-        // consider those in the check.
-        // This prevents duplicated classes when using custom build types. That is, a custom build
-        // type like profile is used, and the plugin and app projects have API dependencies on the
-        // embedding.
-        List<Map<String, Object>> pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency = flutterBuildMode == "release" ? getPluginListWithoutDevDependencies(project) : getPluginList(project);
-        if (!isFlutterAppProject() || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.size() == 0) {
-            FlutterPluginUtils.addApiDependencies(project, buildType.name,
-                    "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
-        }
-        List<String> platforms = getTargetPlatforms().collect()
-        platforms.each { platform ->
-            String arch = FlutterPluginUtils.formatPlatformString(platform)
-            // Add the `libflutter.so` dependency.
-            FlutterPluginUtils.addApiDependencies(project, buildType.name,
-                    "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
-        }
+        FlutterPluginUtils.addFlutterDependencies(project, buildType, getPluginList(project), engineVersion)
     }
 
     /**
@@ -462,8 +395,12 @@
      */
     private void configurePlugins(Project project) {
         configureLegacyPluginEachProjects(project)
-        getPluginList(project).each(this.&configurePluginProject)
-        getPluginList(project).each(this.&configurePluginDependencies)
+        getPluginList(project).each { Map<String, Object> plugin ->
+            FlutterPluginUtils.configurePluginProject(project, plugin, engineVersion)
+        }
+        getPluginList(project).each {Map<String, Object> plugin ->
+            FlutterPluginUtils.configurePluginDependencies(project, plugin)
+        }
     }
 
     // TODO(54566, 48918): Can remove once the issues are resolved.
@@ -516,7 +453,7 @@
             } else if (FlutterPluginUtils.pluginSupportsAndroidPlatform(pluginProject)) {
                 // Plugin has a functioning `android` folder and is included successfully, although it's not supported.
                 // It must be configured nonetheless, to not throw an "Unresolved reference" exception.
-                configurePluginProject(it)
+                FlutterPluginUtils.configurePluginProject(project, it, engineVersion)
             /* groovylint-disable-next-line EmptyElseBlock */
             } else {
             // Plugin has no or an empty `android` folder. No action required.
@@ -524,226 +461,6 @@
         }
     }
 
-    /** Adds the plugin project dependency to the app project. */
-    private void configurePluginProject(Map<String, Object> pluginObject) {
-        assert(pluginObject.name instanceof String)
-        Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
-        if (pluginProject == null) {
-            return
-        }
-        // Apply the "flutter" Gradle extension to plugins so that they can use it's vended
-        // compile/target/min sdk values.
-        pluginProject.extensions.create("flutter", FlutterExtension)
-
-        // Add plugin dependency to the app project. We only want to add dependency
-        // for dev dependencies in non-release builds.
-        project.afterEvaluate {
-            project.android.buildTypes.all { buildType ->
-                if (!pluginObject.dev_dependency || buildType.name != 'release') {
-                    project.dependencies.add("${buildType.name}Api", pluginProject)
-                }
-            }
-        }
-
-        Closure addEmbeddingDependencyToPlugin = { BuildType buildType ->
-            String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
-            // In AGP 3.5, the embedding must be added as an API implementation,
-            // so java8 features are desugared against the runtime classpath.
-            // For more, see https://github.com/flutter/flutter/issues/40126
-            if (!FlutterPluginUtils.supportsBuildMode(project, flutterBuildMode)) {
-                return
-            }
-            if (!pluginProject.hasProperty("android")) {
-                return
-            }
-            // Copy build types from the app to the plugin.
-            // This allows to build apps with plugins and custom build types or flavors.
-            pluginProject.android.buildTypes {
-                "${buildType.name}" {}
-            }
-            // The embedding is API dependency of the plugin, so the AGP is able to desugar
-            // default method implementations when the interface is implemented by a plugin.
-            //
-            // See https://issuetracker.google.com/139821726, and
-            // https://github.com/flutter/flutter/issues/72185 for more details.
-            FlutterPluginUtils.addApiDependencies(
-              pluginProject,
-              buildType.name,
-              "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
-            )
-        }
-
-        // Wait until the Android plugin loaded.
-        pluginProject.afterEvaluate {
-            // Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
-            if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
-                project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
-                project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
-            }
-
-            project.android.buildTypes.all(addEmbeddingDependencyToPlugin)
-        }
-    }
-
-    private void forceNdkDownload(Project gradleProject, String flutterSdkRootPath) {
-        // If the project is already configuring a native build, we don't need to do anything.
-        Boolean forcingNotRequired = gradleProject.android.externalNativeBuild.cmake.path != null
-        if (forcingNotRequired) {
-            return
-        }
-
-        // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings.
-        gradleProject.android {
-            externalNativeBuild {
-                cmake {
-                    // Respect the existing configuration if it exists - the NDK will already be
-                    // downloaded in this case.
-                    path = flutterSdkRootPath + "/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt"
-                }
-            }
-
-            defaultConfig {
-                externalNativeBuild {
-                    cmake {
-                        // CMake will print warnings when you try to build an empty project.
-                        // These arguments silence the warnings - our project is intentionally
-                        // empty.
-                        arguments("-Wno-dev", "--no-warn-unused-cli")
-                    }
-                }
-            }
-        }
-    }
-
-    /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
-    private void detectLowCompileSdkVersionOrNdkVersion() {
-        project.afterEvaluate {
-            // Default to int max if using a preview version to skip the sdk check.
-            int projectCompileSdkVersion = Integer.MAX_VALUE
-            // Stable versions use ints, legacy preview uses string.
-            if (getCompileSdkFromProject(project).isInteger()) {
-                projectCompileSdkVersion = getCompileSdkFromProject(project) as int
-            }
-            int maxPluginCompileSdkVersion = projectCompileSdkVersion
-            String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
-            String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
-            String maxPluginNdkVersion = projectNdkVersion
-            int numProcessedPlugins = getPluginList(project).size()
-            List<Tuple2<String, String>> pluginsWithHigherSdkVersion = []
-            List<Tuple2<String, String>> pluginsWithDifferentNdkVersion = []
-
-            getPluginList(project).each { pluginObject ->
-                assert(pluginObject.name instanceof String)
-                Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
-                if (pluginProject == null) {
-                    return
-                }
-                pluginProject.afterEvaluate {
-                    // Default to int min if using a preview version to skip the sdk check.
-                    int pluginCompileSdkVersion = Integer.MIN_VALUE
-                    // Stable versions use ints, legacy preview uses string.
-                    if (getCompileSdkFromProject(pluginProject).isInteger()) {
-                        pluginCompileSdkVersion = getCompileSdkFromProject(pluginProject) as int
-                    }
-
-                    maxPluginCompileSdkVersion = Math.max(pluginCompileSdkVersion, maxPluginCompileSdkVersion)
-                    if (pluginCompileSdkVersion > projectCompileSdkVersion) {
-                        pluginsWithHigherSdkVersion.add(new Tuple(pluginProject.name, pluginCompileSdkVersion))
-                    }
-
-                    String pluginNdkVersion = pluginProject.android.ndkVersion ?: ndkVersionIfUnspecified
-                    maxPluginNdkVersion = VersionUtils.mostRecentSemanticVersion(pluginNdkVersion, maxPluginNdkVersion)
-                    if (pluginNdkVersion != projectNdkVersion) {
-                        pluginsWithDifferentNdkVersion.add(new Tuple(pluginProject.name, pluginNdkVersion))
-                    }
-
-                    numProcessedPlugins--
-                    if (numProcessedPlugins == 0) {
-                        if (maxPluginCompileSdkVersion > projectCompileSdkVersion) {
-                            project.logger.error("Your project is configured to compile against Android SDK $projectCompileSdkVersion, but the following plugin(s) require to be compiled against a higher Android SDK version:")
-                            for (Tuple2<String, String> pluginToCompileSdkVersion : pluginsWithHigherSdkVersion) {
-                                project.logger.error("- ${pluginToCompileSdkVersion.v1} compiles against Android SDK ${pluginToCompileSdkVersion.v2}")
-                            }
-                            File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger)
-                            project.logger.error("""\
-                                Fix this issue by compiling against the highest Android SDK version (they are backward compatible).
-                                Add the following to ${buildGradleFile.path}:
-
-                                    android {
-                                        compileSdk = ${maxPluginCompileSdkVersion}
-                                        ...
-                                    }
-                                """.stripIndent())
-                        }
-                        if (maxPluginNdkVersion != projectNdkVersion) {
-                            project.logger.error("Your project is configured with Android NDK $projectNdkVersion, but the following plugin(s) depend on a different Android NDK version:")
-                            for (Tuple2<String, String> pluginToNdkVersion : pluginsWithDifferentNdkVersion) {
-                                project.logger.error("- ${pluginToNdkVersion.v1} requires Android NDK ${pluginToNdkVersion.v2}")
-                            }
-                            File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger)
-                            project.logger.error("""\
-                                Fix this issue by using the highest Android NDK version (they are backward compatible).
-                                Add the following to ${buildGradleFile.path}:
-
-                                    android {
-                                        ndkVersion = \"${maxPluginNdkVersion}\"
-                                        ...
-                                    }
-                                """.stripIndent())
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns the portion of the compileSdkVersion string that corresponds to either the numeric
-     * or string version.
-     */
-    private static String getCompileSdkFromProject(Project gradleProject) {
-        return gradleProject.android.compileSdkVersion.substring(8)
-    }
-
-    /**
-     * Add the dependencies on other plugin projects to the plugin project.
-     * A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
-     * making the Gradle plugin project A depend on the Gradle plugin project B.
-     */
-    private void configurePluginDependencies(Map<String, Object> pluginObject) {
-        assert(pluginObject.name instanceof String)
-        Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
-        if (pluginProject == null) {
-            return
-        }
-
-        project.android.buildTypes.each { buildType ->
-            String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType)
-            if (flutterBuildMode == "release" && pluginObject.dev_dependency) {
-                // This plugin is a dev dependency will not be included in the
-                // release build, so no need to add its dependencies.
-                return
-            }
-            def dependencies = pluginObject.dependencies
-            assert(dependencies instanceof List<String>)
-            dependencies.each { pluginDependencyName ->
-                if (pluginDependencyName.empty) {
-                    return
-                }
-                Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
-                if (dependencyProject == null) {
-                    return
-                }
-                // Wait for the Android plugin to load and add the dependency to the plugin project.
-                pluginProject.afterEvaluate {
-                    pluginProject.dependencies {
-                        implementation(dependencyProject)
-                    }
-                }
-            }
-        }
-    }
-
     /**
      * Gets the list of plugins (as map) that support the Android platform.
      *
@@ -758,24 +475,6 @@
         return pluginList
     }
 
-    /**
-     * Gets the list of plugins (as map) that support the Android platform and are dependencies of the
-     * Android project excluding dev dependencies.
-     *
-     * The map value contains either the plugins `name` (String),
-     * its `path` (String), or its `dependencies` (List<String>).
-     * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
-     */
-    private List<Map<String, Object>> getPluginListWithoutDevDependencies(Project project) {
-        List<Map<String, Object>> pluginListWithoutDevDependencies = []
-        for (Map<String, Object> plugin in getPluginList(project)) {
-            if (!plugin.dev_dependency) {
-                pluginListWithoutDevDependencies += plugin
-            }
-        }
-        return pluginListWithoutDevDependencies
-    }
-
     // TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
     //  https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
     /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
@@ -794,28 +493,11 @@
 
     private String resolveProperty(String name, String defaultValue) {
         if (localProperties == null) {
-            localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
+            localProperties = FlutterPluginUtils.readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
         }
         return project.findProperty(name) ?: localProperties?.getProperty(name, defaultValue)
     }
 
-    private List<String> getTargetPlatforms() {
-        final String propTargetPlatform = "target-platform"
-        if (!project.hasProperty(propTargetPlatform)) {
-            return FlutterPluginConstants.DEFAULT_PLATFORMS
-        }
-        return project.property(propTargetPlatform).split(",").collect {
-            if (!FlutterPluginConstants.PLATFORM_ARCH_MAP[it]) {
-                throw new GradleException("Invalid platform: $it.")
-            }
-            return it
-        }
-    }
-
-    private boolean isFlutterAppProject() {
-        return project.android.hasProperty("applicationVariants")
-    }
-
     private void addFlutterTasks(Project project) {
         if (project.state.failure) {
             return
@@ -890,12 +572,12 @@
         if (project.hasProperty(propValidateDeferredComponents)) {
             validateDeferredComponentsValue = project.property(propValidateDeferredComponents).toBoolean()
         }
-        addTaskForJavaVersion(project)
-        if (isFlutterAppProject()) {
-            addTaskForPrintBuildVariants(project)
+        FlutterPluginUtils.addTaskForJavaVersion(project)
+        if (FlutterPluginUtils.isFlutterAppProject(project)) {
+            FlutterPluginUtils.addTaskForPrintBuildVariants(project)
             addTasksForOutputsAppLinkSettings(project)
         }
-        List<String> targetPlatforms = getTargetPlatforms()
+        List<String> targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project)
         def addFlutterDeps = { variant ->
             if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) {
                 variant.outputs.each { output ->
@@ -1058,7 +740,7 @@
             }
             return copyFlutterAssetsTask
         } // end def addFlutterDeps
-        if (isFlutterAppProject()) {
+        if (FlutterPluginUtils.isFlutterAppProject(project)) {
             AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android")
             android.applicationVariants.configureEach { variant ->
                 Task assembleTask = variant.assembleProvider.get()
@@ -1110,7 +792,7 @@
             String nativeAssetsDir = "${project.layout.buildDirectory.get()}/../native_assets/android/jniLibs/lib/"
             android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
             configurePlugins(project)
-            detectLowCompileSdkVersionOrNdkVersion()
+            FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
             return
         }
         // Flutter host module project (Add-to-app).
@@ -1160,6 +842,6 @@
             }
         }
         configurePlugins(project)
-        detectLowCompileSdkVersionOrNdkVersion()
+        FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, getPluginList(project))
     }
 }
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
index fca6b99..eefcd25 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt
@@ -18,6 +18,7 @@
     const val INTERMEDIATES_DIR = "intermediates"
     const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL"
     const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
+    const val WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG = "https://flutter.dev/to/review-gradle-config"
 
     /** Maps platforms to ABI architectures. */
     @JvmStatic val PLATFORM_ARCH_MAP =
diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
index 96bdef7..2e473f6 100644
--- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
+++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
@@ -1,19 +1,26 @@
 package com.flutter.gradle
 
+import com.android.build.gradle.AbstractAppExtension
+import com.android.build.gradle.BaseExtension
 import com.android.builder.model.BuildType
 import groovy.lang.Closure
 import org.gradle.api.GradleException
+import org.gradle.api.JavaVersion
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.api.UnknownTaskException
 import org.gradle.api.logging.Logger
 import java.io.File
+import java.nio.charset.StandardCharsets
+import java.util.Properties
 
 /**
  * A collection of static utility functions used by the Flutter Gradle Plugin.
  */
 object FlutterPluginUtils {
-    // Gradle properties.
+    // Gradle properties. These must correspond to the values used in
+    // flutter/packages/flutter_tools/lib/src/android/gradle.dart, and therefore it is not
+    // recommended to use these const values in tests.
     internal const val PROP_SHOULD_SHRINK_RESOURCES = "shrink"
     internal const val PROP_SPLIT_PER_ABI = "split-per-abi"
     internal const val PROP_LOCAL_ENGINE_REPO = "local-engine-repo"
@@ -21,6 +28,7 @@
     internal const val PROP_IS_FAST_START = "fast-start"
     internal const val PROP_TARGET = "target"
     internal const val PROP_LOCAL_ENGINE_BUILD_MODE = "local-engine-build-mode"
+    internal const val PROP_TARGET_PLATFORM = "target-platform"
 
     // ----------------- Methods for string manipulation and comparison. -----------------
 
@@ -89,6 +97,21 @@
     @JvmName("formatPlatformString")
     fun formatPlatformString(platform: String): String = FlutterPluginConstants.PLATFORM_ARCH_MAP[platform]!!.replace("-", "_")
 
+    @JvmStatic
+    @JvmName("readPropertiesIfExist")
+    internal fun readPropertiesIfExist(propertiesFile: File): Properties {
+        val result = Properties()
+        if (propertiesFile.exists()) {
+            propertiesFile
+                .reader(StandardCharsets.UTF_8)
+                .use { reader ->
+                    // Use Kotlin's reader with UTF-8 and 'use' for auto-closing
+                    result.load(reader)
+                }
+        }
+        return result
+    }
+
     // ----------------- Methods that interact primarily with the Gradle project. -----------------
 
     @JvmStatic
@@ -193,8 +216,6 @@
             )?.toString()
             ?.toBoolean() ?: false
 
-//   TODO(gmackall): @JvmStatic internal fun getCompileSdkFromProject(project: Project): String {}
-
     /**
      * TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560
      *
@@ -371,4 +392,466 @@
         // doesn't support.
         return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode
     }
+
+    private fun getAndroidExtension(project: Project): BaseExtension {
+        // Common supertype of the android extension types.
+        // But maybe this should be https://developer.android.com/reference/tools/gradle-api/8.7/com/android/build/api/dsl/TestedExtension.
+        return project.extensions.findByType(BaseExtension::class.java)!!
+    }
+
+    /**
+     * Expected format of getAndroidExtension(project).compileSdkVersion is a string of the form
+     * `android-` followed by either the numeric version, e.g. `android-35`, or a preview version,
+     * e.g. `android-UpsideDownCake`.
+     */
+    @JvmStatic
+    @JvmName("getCompileSdkFromProject")
+    internal fun getCompileSdkFromProject(project: Project): String = getAndroidExtension(project).compileSdkVersion!!.substring(8)
+
+    /**
+     * Returns:
+     *  The default platforms if the `target-platform` property is not set.
+     *  The requested platforms after verifying they are supported by the Flutter plugin, otherwise.
+     * Throws a GradleException if any of the requested platforms are not supported.
+     */
+    @JvmStatic
+    @JvmName("getTargetPlatforms")
+    internal fun getTargetPlatforms(project: Project): List<String> {
+        if (!project.hasProperty(PROP_TARGET_PLATFORM)) {
+            return FlutterPluginConstants.DEFAULT_PLATFORMS
+        }
+        val platformsString = project.property(PROP_TARGET_PLATFORM) as String
+        return platformsString.split(",").map { platform ->
+            if (!FlutterPluginConstants.PLATFORM_ARCH_MAP.containsKey(platform)) {
+                throw GradleException("Invalid platform: $platform")
+            }
+            platform
+        }
+    }
+
+    private fun logPluginCompileSdkWarnings(
+        maxPluginCompileSdkVersion: Int,
+        projectCompileSdkVersion: Int,
+        logger: Logger,
+        pluginsWithHigherSdkVersion: List<PluginVersionPair>,
+        projectDirectory: File
+    ) {
+        logger.error(
+            "Your project is configured to compile against Android SDK $projectCompileSdkVersion, but the following plugin(s) require to be compiled against a higher Android SDK version:"
+        )
+        for (pluginToCompileSdkVersion in pluginsWithHigherSdkVersion) {
+            logger.error(
+                "- ${pluginToCompileSdkVersion.name} compiles against Android SDK ${pluginToCompileSdkVersion.version}"
+            )
+        }
+        val buildGradleFile =
+            getBuildGradleFileFromProjectDir(
+                projectDirectory,
+                logger
+            )
+        logger.error(
+            """
+            Fix this issue by compiling against the highest Android SDK version (they are backward compatible).
+            Add the following to ${buildGradleFile.path}:
+
+                android {
+                    compileSdk = $maxPluginCompileSdkVersion
+                    ...
+                }
+            """.trimIndent()
+        )
+    }
+
+    private fun logPluginNdkWarnings(
+        maxPluginNdkVersion: String,
+        projectNdkVersion: String,
+        logger: Logger,
+        pluginsWithDifferentNdkVersion: List<PluginVersionPair>,
+        projectDirectory: File
+    ) {
+        logger.error(
+            "Your project is configured with Android NDK $projectNdkVersion, but the following plugin(s) depend on a different Android NDK version:"
+        )
+        for (pluginToNdkVersion in pluginsWithDifferentNdkVersion) {
+            logger.error("- ${pluginToNdkVersion.name} requires Android NDK ${pluginToNdkVersion.version}")
+        }
+        val buildGradleFile =
+            getBuildGradleFileFromProjectDir(
+                projectDirectory,
+                logger
+            )
+        logger.error(
+            """
+            Fix this issue by using the highest Android NDK version (they are backward compatible).
+            Add the following to ${buildGradleFile.path}:
+
+                android {
+                    ndkVersion = "$maxPluginNdkVersion"
+                    ...
+                }
+            """.trimIndent()
+        )
+    }
+
+    /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
+    @JvmStatic
+    @JvmName("detectLowCompileSdkVersionOrNdkVersion")
+    internal fun detectLowCompileSdkVersionOrNdkVersion(
+        project: Project,
+        pluginList: List<Map<String?, Any?>>
+    ) {
+        project.afterEvaluate {
+            // getCompileSdkFromProject returns a string if the project uses a preview compileSdkVersion
+            // so default to Int.MAX_VALUE in that case.
+            val projectCompileSdkVersion: Int =
+                getCompileSdkFromProject(project).toIntOrNull() ?: Int.MAX_VALUE
+
+            var maxPluginCompileSdkVersion = projectCompileSdkVersion
+            // TODO(gmackall): This should be updated to reflect newer templates.
+            // The default for AGP 4.1.0 used in old templates.
+            val ndkVersionIfUnspecified = "21.1.6352462"
+            val projectNdkVersion =
+                getAndroidExtension(project).ndkVersion ?: ndkVersionIfUnspecified
+            var maxPluginNdkVersion = projectNdkVersion
+            var numProcessedPlugins = pluginList.size
+            val pluginsWithHigherSdkVersion = mutableListOf<PluginVersionPair>()
+            val pluginsWithDifferentNdkVersion = mutableListOf<PluginVersionPair>()
+            pluginList.forEach { pluginObject ->
+                val pluginName: String =
+                    requireNotNull(
+                        pluginObject["name"] as? String
+                    ) { "Missing valid \"name\" property for plugin object: $pluginObject" }
+                val pluginProject: Project =
+                    project.rootProject.findProject(":$pluginName") ?: return@forEach
+                pluginProject.afterEvaluate {
+                    val pluginCompileSdkVersion: Int =
+                        getCompileSdkFromProject(pluginProject).toIntOrNull() ?: Int.MAX_VALUE
+                    maxPluginCompileSdkVersion =
+                        maxOf(maxPluginCompileSdkVersion, pluginCompileSdkVersion)
+                    if (pluginCompileSdkVersion > projectCompileSdkVersion) {
+                        pluginsWithHigherSdkVersion.add(
+                            PluginVersionPair(
+                                pluginName,
+                                pluginCompileSdkVersion.toString()
+                            )
+                        )
+                    }
+                    val pluginNdkVersion: String =
+                        getAndroidExtension(pluginProject).ndkVersion ?: ndkVersionIfUnspecified
+                    maxPluginNdkVersion =
+                        VersionUtils.mostRecentSemanticVersion(
+                            pluginNdkVersion,
+                            maxPluginNdkVersion
+                        )
+                    if (pluginNdkVersion != projectNdkVersion) {
+                        pluginsWithDifferentNdkVersion.add(PluginVersionPair(pluginName, pluginNdkVersion))
+                    }
+
+                    numProcessedPlugins--
+                    if (numProcessedPlugins == 0) {
+                        if (maxPluginCompileSdkVersion > projectCompileSdkVersion) {
+                            logPluginCompileSdkWarnings(
+                                maxPluginCompileSdkVersion = maxPluginCompileSdkVersion,
+                                projectCompileSdkVersion = projectCompileSdkVersion,
+                                logger = project.logger,
+                                pluginsWithHigherSdkVersion = pluginsWithHigherSdkVersion,
+                                projectDirectory = project.projectDir
+                            )
+                        }
+                        if (maxPluginNdkVersion != projectNdkVersion) {
+                            logPluginNdkWarnings(
+                                maxPluginNdkVersion = maxPluginNdkVersion,
+                                projectNdkVersion = projectNdkVersion,
+                                logger = project.logger,
+                                pluginsWithDifferentNdkVersion = pluginsWithDifferentNdkVersion,
+                                projectDirectory = project.projectDir
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Forces the project to download the NDK by configuring properties that makes AGP think the
+     * project actually requires the NDK.
+     */
+    @JvmStatic
+    @JvmName("forceNdkDownload")
+    internal fun forceNdkDownload(
+        gradleProject: Project,
+        flutterSdkRootPath: String
+    ) {
+        // If the project is already configuring a native build, we don't need to do anything.
+        val gradleProjectAndroidExtension = getAndroidExtension(gradleProject)
+        val forcingNotRequired: Boolean =
+            gradleProjectAndroidExtension.externalNativeBuild.cmake.path != null
+        if (forcingNotRequired) {
+            return
+        }
+
+        // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings.
+        gradleProjectAndroidExtension.externalNativeBuild.cmake.path(
+            "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt"
+        )
+
+        // CMake will print warnings when you try to build an empty project.
+        // These arguments silence the warnings - our project is intentionally
+        // empty.
+        gradleProjectAndroidExtension.defaultConfig.externalNativeBuild.cmake
+            .arguments("-Wno-dev", "--no-warn-unused-cli")
+    }
+
+    @JvmStatic
+    @JvmName("isFlutterAppProject")
+    internal fun isFlutterAppProject(project: Project): Boolean = project.extensions.findByType(AbstractAppExtension::class.java) != null
+
+    /**
+     * Ensures that the dependencies required by the Flutter project are available.
+     * This includes:
+     *    1. The embedding
+     *    2. libflutter.so
+     *
+     * Should only be called on the main gradle [Project] for this application
+     * of the [FlutterPlugin].
+     */
+    @JvmStatic
+    @JvmName("addFlutterDependencies")
+    internal fun addFlutterDependencies(
+        project: Project,
+        buildType: BuildType,
+        pluginList: List<Map<String?, Any?>>,
+        engineVersion: String
+    ) {
+        val flutterBuildMode: String = buildModeFor(buildType)
+        if (!supportsBuildMode(project, flutterBuildMode)) {
+            project.logger.quiet(
+                "Project does not support Flutter build mode: $flutterBuildMode, " +
+                    "skipping adding flutter dependencies"
+            )
+            return
+        }
+        // The embedding is set as an API dependency in a Flutter plugin.
+        // Therefore, don't make the app project depend on the embedding if there are Flutter
+        // plugin dependencies. In release mode, dev dependencies are stripped, so we do not
+        // consider those in the check.
+        // This prevents duplicated classes when using custom build types. That is, a custom build
+        // type like profile is used, and the plugin and app projects have API dependencies on the
+        // embedding.
+        val pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency: List<Map<String?, Any?>> =
+            if (flutterBuildMode == "release") {
+                getPluginListWithoutDevDependencies(
+                    pluginList
+                )
+            } else {
+                pluginList
+            }
+
+        if (!isFlutterAppProject(project) || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.isEmpty()) {
+            addApiDependencies(
+                project,
+                buildType.name,
+                "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
+            )
+        }
+        val platforms: List<String> = getTargetPlatforms(project)
+        platforms.forEach { platform ->
+            val arch: String = formatPlatformString(platform)
+            // Add the `libflutter.so` dependency.
+            addApiDependencies(
+                project,
+                buildType.name,
+                "io.flutter:${arch}_$flutterBuildMode:$engineVersion"
+            )
+        }
+    }
+
+    /**
+     * Gets the list of plugins (as map) that support the Android platform and are dependencies of the
+     * Android project excluding dev dependencies.
+     *
+     * The map value contains either the plugins `name` (String),
+     * its `path` (String), or its `dependencies` (List<String>).
+     * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
+     */
+    private fun getPluginListWithoutDevDependencies(pluginList: List<Map<String?, Any?>>): List<Map<String?, Any?>> =
+        pluginList.filter { pluginObject -> pluginObject["dev_dependency"] == false }
+
+    /**
+     * Add the dependencies on other plugin projects to the plugin project.
+     * A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
+     * making the Gradle plugin project A depend on the Gradle plugin project B.
+     */
+    @JvmStatic
+    @JvmName("configurePluginDependencies")
+    internal fun configurePluginDependencies(
+        project: Project,
+        pluginObject: Map<String?, Any?>
+    ) {
+        val pluginName: String =
+            requireNotNull(pluginObject["name"] as? String) {
+                "Missing valid \"name\" property for plugin object: $pluginObject"
+            }
+        val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
+
+        getAndroidExtension(project).buildTypes.forEach { buildType ->
+            val flutterBuildMode: String = buildModeFor(buildType)
+            if (flutterBuildMode == "release" && (pluginObject["dev_dependency"] as? Boolean == true)) {
+                // This plugin is a dev dependency will not be included in the
+                // release build, so no need to add its dependencies.
+                return@forEach
+            }
+            val dependencies = requireNotNull(pluginObject["dependencies"] as? List<*>)
+            dependencies.forEach innerForEach@{ pluginDependencyName ->
+                check(pluginDependencyName is String)
+                if (pluginDependencyName.isEmpty()) {
+                    return@innerForEach
+                }
+
+                val dependencyProject =
+                    project.rootProject.findProject(":$pluginDependencyName") ?: return@innerForEach
+                pluginProject.afterEvaluate {
+                    pluginProject.dependencies.add("implementation", dependencyProject)
+                }
+            }
+        }
+    }
+
+    /**
+     * Performs configuration related to the plugin's Gradle [Project], including
+     * 1. Adding the plugin itself as a dependency to the main project.
+     * 2. Adding the main project's build types to the plugin's build types.
+     * 3. Adding a dependency on the Flutter embedding to the plugin.
+     *
+     * Should only be called on plugins that support the Android platform.
+     */
+    @JvmStatic
+    @JvmName("configurePluginProject")
+    internal fun configurePluginProject(
+        project: Project,
+        pluginObject: Map<String?, Any?>,
+        engineVersion: String
+    ) {
+        // TODO(gmackall): should guard this with a pluginObject.contains().
+        val pluginName =
+            requireNotNull(pluginObject["name"] as? String) { "Plugin name must be a string for plugin object: $pluginObject" }
+        val pluginProject: Project = project.rootProject.findProject(":$pluginName") ?: return
+
+        // Apply the "flutter" Gradle extension to plugins so that they can use it's vended
+        // compile/target/min sdk values.
+        pluginProject.extensions.create("flutter", FlutterExtension::class.java)
+
+        // Add plugin dependency to the app project. We only want to add dependency
+        // for dev dependencies in non-release builds.
+        project.afterEvaluate {
+            getAndroidExtension(project).buildTypes.forEach { buildType ->
+                if (!(pluginObject["dev_dependency"] as Boolean) || buildType.name != "release") {
+                    project.dependencies.add("${buildType.name}Api", pluginProject)
+                }
+            }
+        }
+
+        // Wait until the Android plugin loaded.
+        pluginProject.afterEvaluate {
+            // Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
+            val projectCompileSdkVersion: String = getCompileSdkFromProject(project)
+            val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject)
+            // TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong
+            //                 when comparing preview versions (against non preview, and also in the
+            //                 case of alphabet reset which happened with "Baklava".
+            if (pluginCompileSdkVersion > projectCompileSdkVersion) {
+                project.logger.quiet("Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher.")
+                project.logger.quiet(
+                    "For more information about build configuration, see ${FlutterPluginConstants.WEBSITE_DEPLOYMENT_ANDROID_BUILD_CONFIG}."
+                )
+            }
+
+            getAndroidExtension(project).buildTypes.forEach { buildType ->
+                addEmbeddingDependencyToPlugin(project, pluginProject, buildType, engineVersion)
+            }
+        }
+    }
+
+    private fun addEmbeddingDependencyToPlugin(
+        project: Project,
+        pluginProject: Project,
+        buildType: BuildType,
+        engineVersion: String
+    ) {
+        val flutterBuildMode: String = buildModeFor(buildType)
+        // TODO(gmackall): this should be safe to remove, as the minimum required AGP is well above
+        //                 3.5. We should try to remove it.
+        // In AGP 3.5, the embedding must be added as an API implementation,
+        // so java8 features are desugared against the runtime classpath.
+        // For more, see https://github.com/flutter/flutter/issues/40126
+        if (!supportsBuildMode(pluginProject, flutterBuildMode)) {
+            return
+        }
+        if (!pluginProject.hasProperty("android")) {
+            return
+        }
+
+        // Copy build types from the app to the plugin.
+        // This allows to build apps with plugins and custom build types or flavors.
+        getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes)
+
+        // The embedding is API dependency of the plugin, so the AGP is able to desugar
+        // default method implementations when the interface is implemented by a plugin.
+        //
+        // See https://issuetracker.google.com/139821726, and
+        // https://github.com/flutter/flutter/issues/72185 for more details.
+        addApiDependencies(pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
+    }
+
+    // ------------------ Task adders (a subset of the above category)
+
+    // Add a task that can be called on flutter projects that prints the Java version used in Gradle.
+    //
+    // Format of the output of this task can be used in debugging what version of Java Gradle is using.
+    // Not recommended for use in time sensitive commands like `flutter run` or `flutter build` as
+    // Gradle is slower than we want. Particularly in light of https://github.com/flutter/flutter/issues/119196.
+    @JvmStatic
+    @JvmName("addTaskForJavaVersion")
+    internal fun addTaskForJavaVersion(project: Project) {
+        project.tasks.register("javaVersion") {
+            description = "Print the current java version used by gradle. see: " +
+                "https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
+            doLast {
+                println(JavaVersion.current())
+            }
+        }
+    }
+
+    // Add a task that can be called on Flutter projects that prints the available build variants
+    // in Gradle.
+    //
+    // This task prints variants in this format:
+    //
+    // BuildVariant: debug
+    // BuildVariant: release
+    // BuildVariant: profile
+    //
+    // Format of the output of this task is used by `AndroidProject.getBuildVariants`.
+    @JvmStatic
+    @JvmName("addTaskForPrintBuildVariants")
+    internal fun addTaskForPrintBuildVariants(project: Project) {
+        // Groovy was dynamically getting a different subtype here than our Kotlin getAndroidExtension method.
+        // TODO(gmackall): We should take another pass at the different types we are using in our conversion of
+        //                 the groovy `flutter.android` lines.
+        val androidExtension = project.extensions.getByType(AbstractAppExtension::class.java)
+        project.tasks.register("printBuildVariants") {
+            description = "Prints out all build variants for this Android project"
+            doLast {
+                androidExtension.applicationVariants.forEach { variant ->
+                    println("BuildVariant: ${variant.name}")
+                }
+            }
+        }
+    }
 }
+
+private data class PluginVersionPair(
+    val name: String,
+    val version: String
+)
diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
index cbc3062..dcad62a 100644
--- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
+++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt
@@ -1,19 +1,31 @@
 package com.flutter.gradle
 
+import com.android.build.gradle.AbstractAppExtension
+import com.android.build.gradle.BaseExtension
+import com.android.build.gradle.internal.dsl.CmakeOptions
+import com.android.build.gradle.internal.dsl.DefaultConfig
 import com.android.builder.model.BuildType
+import io.mockk.called
 import io.mockk.every
 import io.mockk.mockk
+import io.mockk.slot
 import io.mockk.verify
+import org.gradle.api.Action
 import org.gradle.api.GradleException
+import org.gradle.api.NamedDomainObjectContainer
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.api.UnknownTaskException
+import org.gradle.api.artifacts.dsl.DependencyHandler
 import org.gradle.api.logging.Logger
 import org.junit.jupiter.api.assertThrows
 import org.junit.jupiter.api.io.TempDir
 import java.io.File
 import java.nio.file.Path
+import java.util.Properties
+import kotlin.io.path.createDirectory
 import kotlin.test.Test
+import kotlin.test.assertContains
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
@@ -138,7 +150,8 @@
         val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle")
         settingsGradle.createNewFile()
 
-        val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk())
+        val result =
+            FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk())
         assertEquals(settingsGradle, result)
     }
 
@@ -157,7 +170,8 @@
         val mockLogger = mockk<Logger>()
         every { mockLogger.error(any()) } returns Unit
 
-        val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
+        val result =
+            FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
         assertEquals(groovySettingsGradle, result)
         verify { mockLogger.error(any()) }
     }
@@ -173,7 +187,8 @@
         val buildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle")
         buildGradle.createNewFile()
 
-        val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk())
+        val result =
+            FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk())
         assertEquals(buildGradle, result)
     }
 
@@ -192,7 +207,8 @@
         val mockLogger = mockk<Logger>()
         every { mockLogger.error(any()) } returns Unit
 
-        val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
+        val result =
+            FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger)
         assertEquals(groovyBuildGradle, result)
         verify { mockLogger.error(any()) }
     }
@@ -358,7 +374,11 @@
         val variantName = "debug"
         val dependency = mockk<Any>()
 
-        every { project.configurations.named("api") } throws UnknownTaskException("message", mockk())
+        every { project.configurations.named("api") } throws
+            UnknownTaskException(
+                "message",
+                mockk()
+            )
         every { project.dependencies.add(any(), any()) } returns mockk()
 
         FlutterPluginUtils.addApiDependencies(project, variantName, dependency)
@@ -415,4 +435,750 @@
         val result = FlutterPluginUtils.supportsBuildMode(project, "release")
         assertEquals(false, result)
     }
+
+    // getTargetPlatforms
+    @Test
+    fun `getTargetPlatforms the default if property is not set`() {
+        val project = mockk<Project>()
+        every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns false
+        val result = FlutterPluginUtils.getTargetPlatforms(project)
+        assertEquals(listOf("android-arm", "android-arm64", "android-x64"), result)
+    }
+
+    @Test
+    fun `getTargetPlatforms the value if property is set`() {
+        val project = mockk<Project>()
+        every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
+        every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-arm64,android-arm"
+        val result = FlutterPluginUtils.getTargetPlatforms(project)
+        assertEquals(listOf("android-arm64", "android-arm"), result)
+    }
+
+    @Test
+    fun `getTargetPlatforms throws GradleException if property is set to invalid value`() {
+        val project = mockk<Project>()
+        every { project.hasProperty(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns true
+        every { project.property(FlutterPluginUtils.PROP_TARGET_PLATFORM) } returns "android-invalid"
+        val gradleException: GradleException =
+            assertThrows<GradleException> {
+                FlutterPluginUtils.getTargetPlatforms(project)
+            }
+        assertContains(gradleException.message!!, "android-invalid")
+    }
+
+    // readPropertiesIfExist
+    @Test
+    fun `readPropertiesIfExist returns empty Properties when file does not exist`(
+        @TempDir tempDir: Path
+    ) {
+        val propertiesFile = tempDir.resolve("file_that_doesnt_exist.properties")
+        val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile.toFile())
+        assertEquals(Properties(), result)
+    }
+
+    @Test
+    fun `readPropertiesIfExist returns Properties when file exists`(
+        @TempDir tempDir: Path
+    ) {
+        val propertiesFile = tempDir.resolve("file_that_exists.properties").toFile()
+        propertiesFile.writeText(
+            """
+            sdk.dir=/Users/someuser/Library/Android/sdk
+            flutter.sdk=/Users/someuser/development/flutter/flutter
+            flutter.buildMode=release
+            flutter.versionName=1.0.0
+            flutter.versionCode=1
+            """.trimIndent()
+        )
+
+        val result = FlutterPluginUtils.readPropertiesIfExist(propertiesFile)
+        assertEquals(5, result.size)
+        assertEquals("/Users/someuser/Library/Android/sdk", result["sdk.dir"])
+        assertEquals("/Users/someuser/development/flutter/flutter", result["flutter.sdk"])
+        assertEquals("release", result["flutter.buildMode"])
+        assertEquals("1.0.0", result["flutter.versionName"])
+        assertEquals("1", result["flutter.versionCode"])
+    }
+
+    // getCompileSdkFromProject
+    @Test
+    fun `getCompileSdkFromProject returns the compileSdk from the project`() {
+        val project = mockk<Project>()
+        every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        val result = FlutterPluginUtils.getCompileSdkFromProject(project)
+        assertEquals("35", result)
+    }
+
+    // detectLowCompileSdkVersionOrNdkVersion
+    @Test
+    fun `detectLowCompileSdkVersionOrNdkVersion logs no warnings when no plugins have higher sdk or ndk`(
+        @TempDir tempDir: Path
+    ) {
+        val projectDir = tempDir.resolve("app").toFile()
+
+        val project = mockk<Project>()
+        val mockLogger = mockk<Logger>()
+        every { project.logger } returns mockLogger
+        every { project.projectDir } returns projectDir
+        val cameraPluginProject = mockk<Project>()
+        val projectActionSlot = slot<Action<Project>>()
+        val cameraPluginProjectActionSlot = slot<Action<Project>>()
+        every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
+        every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
+        every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject
+        every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit
+        every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
+
+        FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, listOf(cameraDependency))
+
+        verify { project.afterEvaluate(capture(projectActionSlot)) }
+        projectActionSlot.captured.execute(project)
+        verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) }
+        cameraPluginProjectActionSlot.captured.execute(cameraPluginProject)
+
+        verify { mockLogger wasNot called }
+    }
+
+    @Test
+    fun `detectLowCompileSdkVersionOrNdkVersion logs warnings when plugins have higher sdk and ndk`(
+        @TempDir tempDir: Path
+    ) {
+        val buildGradleFile =
+            tempDir
+                .resolve("app")
+                .createDirectory()
+                .resolve("build.gradle")
+                .toFile()
+        buildGradleFile.createNewFile()
+        val projectDir = tempDir.resolve("app").toFile()
+
+        val project = mockk<Project>()
+        val mockLogger = mockk<Logger>()
+        every { project.logger } returns mockLogger
+        every { mockLogger.error(any()) } returns Unit
+        every { project.projectDir } returns projectDir
+        val cameraPluginProject = mockk<Project>()
+        val flutterPluginAndroidLifecycleDependencyPluginProject = mockk<Project>()
+        val projectActionSlot = slot<Action<Project>>()
+        val cameraPluginProjectActionSlot = slot<Action<Project>>()
+        val flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot = slot<Action<Project>>()
+        every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
+        every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-33"
+        every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "24.3.11579264"
+        every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject
+        every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns
+            flutterPluginAndroidLifecycleDependencyPluginProject
+        every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit
+        every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
+        every {
+            flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate(
+                capture(
+                    flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot
+                )
+            )
+        } returns Unit
+        every {
+            flutterPluginAndroidLifecycleDependencyPluginProject.extensions
+                .findByType(
+                    BaseExtension::class.java
+                )!!
+                .compileSdkVersion
+        } returns "android-34"
+        every {
+            flutterPluginAndroidLifecycleDependencyPluginProject.extensions
+                .findByType(
+                    BaseExtension::class.java
+                )!!
+                .ndkVersion
+        } returns "25.3.11579264"
+
+        val dependencyList: List<Map<String?, Any?>> =
+            listOf(cameraDependency, flutterPluginAndroidLifecycleDependency)
+        FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
+            project,
+            dependencyList
+        )
+
+        verify { project.afterEvaluate(capture(projectActionSlot)) }
+        projectActionSlot.captured.execute(project)
+        verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) }
+        cameraPluginProjectActionSlot.captured.execute(cameraPluginProject)
+        verify {
+            flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate(
+                capture(
+                    flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot
+                )
+            )
+        }
+        flutterPluginAndroidLifecycleDependencyPluginProjectActionSlot.captured.execute(
+            flutterPluginAndroidLifecycleDependencyPluginProject
+        )
+
+        verify {
+            mockLogger.error(
+                "Your project is configured to compile against Android SDK 33, but the " +
+                    "following plugin(s) require to be compiled against a higher Android SDK version:"
+            )
+        }
+        verify {
+            mockLogger.error(
+                "- ${cameraDependency["name"]} compiles against Android SDK 35"
+            )
+        }
+        verify {
+            mockLogger.error(
+                "- ${flutterPluginAndroidLifecycleDependency["name"]} compiles against Android SDK 34"
+            )
+        }
+        verify {
+            mockLogger.error(
+                """
+                Fix this issue by compiling against the highest Android SDK version (they are backward compatible).
+                Add the following to ${buildGradleFile.path}:
+
+                    android {
+                        compileSdk = 35
+                        ...
+                    }
+                """.trimIndent()
+            )
+        }
+        verify {
+            mockLogger.error(
+                "Your project is configured with Android NDK 24.3.11579264, but the following plugin(s) depend on a different Android NDK version:"
+            )
+        }
+        verify {
+            mockLogger.error(
+                "- ${cameraDependency["name"]} requires Android NDK 26.3.11579264"
+            )
+        }
+        verify {
+            mockLogger.error(
+                "- ${flutterPluginAndroidLifecycleDependency["name"]} requires Android NDK 25.3.11579264"
+            )
+        }
+        verify {
+            mockLogger.error(
+                """
+                Fix this issue by using the highest Android NDK version (they are backward compatible).
+                Add the following to ${buildGradleFile.path}:
+
+                    android {
+                        ndkVersion = "26.3.11579264"
+                        ...
+                    }
+                """.trimIndent()
+            )
+        }
+    }
+
+    @Test
+    fun `detectLowCompileSdkVersionOrNdkVersion throws IllegalArgumentException when plugin has no name`() {
+        val project = mockk<Project>()
+        val projectActionSlot = slot<Action<Project>>()
+        every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
+        every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264"
+
+        val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
+        pluginWithoutName.remove("name")
+
+        FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(
+            project,
+            listOf(pluginWithoutName)
+        )
+        verify { project.afterEvaluate(capture(projectActionSlot)) }
+        assertThrows<IllegalArgumentException> { projectActionSlot.captured.execute(project) }
+    }
+
+    // forceNdkDownload
+    @Test
+    fun `forceNdkDownload skips projects which are already configuring a native build`(
+        @TempDir tempDir: Path
+    ) {
+        val fakeCmakeFile = tempDir.resolve("CMakeLists.txt").toFile()
+        fakeCmakeFile.createNewFile()
+        val project = mockk<Project>()
+        val mockCmakeOptions = mockk<CmakeOptions>()
+        val mockDefaultConfig = mockk<DefaultConfig>()
+        every {
+            project.extensions
+                .findByType(BaseExtension::class.java)!!
+                .externalNativeBuild.cmake
+        } returns mockCmakeOptions
+        every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
+
+        every { mockCmakeOptions.path } returns fakeCmakeFile
+
+        FlutterPluginUtils.forceNdkDownload(project, "ignored")
+
+        verify(exactly = 1) {
+            mockCmakeOptions.path
+        }
+        verify(exactly = 0) { mockCmakeOptions.setPath(any()) }
+        verify { mockDefaultConfig wasNot called }
+    }
+
+    @Test
+    fun `forceNdkDownload sets externalNativeBuild properties`() {
+        val project = mockk<Project>()
+        val mockCmakeOptions = mockk<CmakeOptions>()
+        val mockDefaultConfig = mockk<DefaultConfig>()
+        every {
+            project.extensions
+                .findByType(BaseExtension::class.java)!!
+                .externalNativeBuild.cmake
+        } returns mockCmakeOptions
+        every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig
+
+        every { mockCmakeOptions.path } returns null
+        every { mockCmakeOptions.path(any()) } returns Unit
+        every { mockDefaultConfig.externalNativeBuild.cmake.arguments(any(), any()) } returns Unit
+
+        val basePath = "/base/path"
+        FlutterPluginUtils.forceNdkDownload(project, basePath)
+
+        verify(exactly = 1) {
+            mockCmakeOptions.path
+        }
+        verify(exactly = 1) { mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/groovy/CMakeLists.txt") }
+        verify(exactly = 1) {
+            mockDefaultConfig.externalNativeBuild.cmake.arguments(
+                "-Wno-dev",
+                "--no-warn-unused-cli"
+            )
+        }
+    }
+
+    // isFlutterAppProject skipped as it is a wrapper for a single getter that we would have to mock
+
+    // addFlutterDependencies
+    @Test
+    fun `addFlutterDependencies returns early if buildMode is not supported`() {
+        val project = mockk<Project>()
+        val buildType: BuildType = mockk<BuildType>()
+        every { buildType.name } returns "debug"
+        every { buildType.isDebuggable } returns true
+        every { project.hasProperty("local-engine-repo") } returns true
+        every { project.hasProperty("local-engine-build-mode") } returns true
+        every { project.property("local-engine-build-mode") } returns "release"
+        every { project.logger.quiet(any()) } returns Unit
+
+        FlutterPluginUtils.addFlutterDependencies(
+            project = project,
+            buildType = buildType,
+            pluginList = pluginListWithoutDevDependency,
+            engineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
+        )
+
+        verify(exactly = 1) {
+            project.logger.quiet(
+                "Project does not support Flutter build mode: debug, " +
+                    "skipping adding flutter dependencies"
+            )
+        }
+    }
+
+    @Test
+    fun `addFlutterDependencies adds libflutter dependency but not embedding dependency when is a flutter app`() {
+        val project = mockk<Project>()
+        val buildType: BuildType = mockk<BuildType>()
+        val engineVersion = exampleEngineVersion
+        every { buildType.name } returns "debug"
+        every { buildType.isDebuggable } returns true
+        every { project.hasProperty("local-engine-repo") } returns false
+        every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
+        every { project.hasProperty("target-platform") } returns false
+        every { project.configurations.named("api") } returns mockk()
+        every { project.dependencies.add(any(), any()) } returns mockk()
+
+        FlutterPluginUtils.addFlutterDependencies(
+            project = project,
+            buildType = buildType,
+            pluginList = pluginListWithoutDevDependency,
+            engineVersion = engineVersion
+        )
+
+        verify(exactly = 3) { project.dependencies.add(any(), any()) }
+        verify {
+            project.dependencies.add(
+                "debugApi",
+                "io.flutter:armeabi_v7a_debug:$engineVersion"
+            )
+        }
+        verify { project.dependencies.add("debugApi", "io.flutter:arm64_v8a_debug:$engineVersion") }
+        verify { project.dependencies.add("debugApi", "io.flutter:x86_64_debug:$engineVersion") }
+    }
+
+    @Test
+    fun `addFlutterDependencies adds libflutter and embedding dep when only dep is dev dep in release mode`() {
+        val project = mockk<Project>()
+        val buildType: BuildType = mockk<BuildType>()
+        val engineVersion = exampleEngineVersion
+        every { buildType.name } returns "release"
+        every { buildType.isDebuggable } returns false
+        every { project.hasProperty("local-engine-repo") } returns false
+        every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
+        every { project.hasProperty("target-platform") } returns false
+        every { project.configurations.named("api") } returns mockk()
+        every { project.dependencies.add(any(), any()) } returns mockk()
+
+        val pluginListWithSingleDevDependency = listOf(devDependency)
+
+        FlutterPluginUtils.addFlutterDependencies(
+            project = project,
+            buildType = buildType,
+            pluginList = pluginListWithSingleDevDependency,
+            engineVersion = engineVersion
+        )
+
+        verify(exactly = 4) { project.dependencies.add(any(), any()) }
+        verify {
+            project.dependencies.add(
+                "releaseApi",
+                "io.flutter:flutter_embedding_release:$engineVersion"
+            )
+        }
+        verify {
+            project.dependencies.add(
+                "releaseApi",
+                "io.flutter:armeabi_v7a_release:$engineVersion"
+            )
+        }
+        verify {
+            project.dependencies.add(
+                "releaseApi",
+                "io.flutter:arm64_v8a_release:$engineVersion"
+            )
+        }
+        verify {
+            project.dependencies.add(
+                "releaseApi",
+                "io.flutter:x86_64_release:$engineVersion"
+            )
+        }
+    }
+
+    @Test
+    fun `addFlutterDependencies adds libflutter dep but not embedding dep when only dep is dev dep in debug mode`() {
+        val project = mockk<Project>()
+        val buildType: BuildType = mockk<BuildType>()
+        val engineVersion = exampleEngineVersion
+        every { buildType.name } returns "debug"
+        every { buildType.isDebuggable } returns true
+        every { project.hasProperty("local-engine-repo") } returns false
+        every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockk<AbstractAppExtension>()
+        every { project.hasProperty("target-platform") } returns false
+        every { project.configurations.named("api") } returns mockk()
+        every { project.dependencies.add(any(), any()) } returns mockk()
+
+        val pluginListWithSingleDevDependency = listOf(devDependency)
+
+        FlutterPluginUtils.addFlutterDependencies(
+            project = project,
+            buildType = buildType,
+            pluginList = pluginListWithSingleDevDependency,
+            engineVersion = engineVersion
+        )
+
+        verify(exactly = 3) { project.dependencies.add(any(), any()) }
+        verify {
+            project.dependencies.add(
+                "debugApi",
+                "io.flutter:armeabi_v7a_debug:$engineVersion"
+            )
+        }
+        verify {
+            project.dependencies.add(
+                "debugApi",
+                "io.flutter:arm64_v8a_debug:$engineVersion"
+            )
+        }
+        verify {
+            project.dependencies.add(
+                "debugApi",
+                "io.flutter:x86_64_debug:$engineVersion"
+            )
+        }
+    }
+
+    // configurePluginDependencies TODO
+    @Test
+    fun `configurePluginDependencies throws IllegalArgumentException when plugin has no name`() {
+        val project = mockk<Project>()
+        val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
+        pluginWithoutName.remove("name")
+        assertThrows<IllegalArgumentException> {
+            FlutterPluginUtils.configurePluginDependencies(
+                project = project,
+                pluginObject = pluginWithoutName
+            )
+        }
+    }
+
+    @Test
+    fun `configurePluginDependencies throws IllegalArgumentException when plugin has null dependencies`() {
+        val project = mockk<Project>()
+        val pluginProject = mockk<Project>()
+        val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
+        val pluginWithNullDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
+        pluginWithNullDependencies["dependencies"] = null
+        every { project.rootProject.findProject(":${pluginWithNullDependencies["name"]}") } returns pluginProject
+        every {
+            project.extensions
+                .findByType(BaseExtension::class.java)!!
+                .buildTypes
+                .iterator()
+        } returns
+            mutableListOf(
+                mockBuildType
+            ).iterator()
+        every { mockBuildType.name } returns "debug"
+        every { mockBuildType.isDebuggable } returns true
+
+        assertThrows<IllegalArgumentException> {
+            FlutterPluginUtils.configurePluginDependencies(
+                project = project,
+                pluginObject = pluginWithNullDependencies
+            )
+        }
+    }
+
+    @Test
+    fun `configurePluginDependencies adds plugin dependencies`() {
+        val project = mockk<Project>()
+        val pluginProject = mockk<Project>()
+        val pluginDependencyProject = mockk<Project>()
+        val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
+        val pluginWithDependencies: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
+        pluginWithDependencies["dependencies"] =
+            listOf(flutterPluginAndroidLifecycleDependency["name"])
+        every { project.rootProject.findProject(":${pluginWithDependencies["name"]}") } returns pluginProject
+        every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns pluginDependencyProject
+        every {
+            project.extensions
+                .findByType(BaseExtension::class.java)!!
+                .buildTypes
+                .iterator()
+        } returns
+            mutableListOf(
+                mockBuildType
+            ).iterator()
+        every { mockBuildType.name } returns "debug"
+        every { mockBuildType.isDebuggable } returns true
+        val captureActionSlot = slot<Action<Project>>()
+        every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
+        val mockDependencyHandler = mockk<DependencyHandler>()
+        every { pluginProject.dependencies } returns mockDependencyHandler
+        every { mockDependencyHandler.add(any(), any()) } returns mockk()
+
+        FlutterPluginUtils.configurePluginDependencies(
+            project = project,
+            pluginObject = pluginWithDependencies
+        )
+
+        verify { pluginProject.afterEvaluate(capture(captureActionSlot)) }
+        captureActionSlot.captured.execute(pluginDependencyProject)
+        verify { mockDependencyHandler.add("implementation", pluginDependencyProject) }
+    }
+
+    // configurePluginProject
+    @Test
+    fun `configurePluginProject throws IllegalArgumentException when plugin has no name`() {
+        val project = mockk<Project>()
+        val pluginWithoutName: MutableMap<String?, Any?> = cameraDependency.toMutableMap()
+        pluginWithoutName.remove("name")
+
+        assertThrows<IllegalArgumentException> {
+            FlutterPluginUtils.configurePluginProject(
+                project = project,
+                pluginObject = pluginWithoutName,
+                engineVersion = exampleEngineVersion
+            )
+        }
+    }
+
+    @Test
+    fun `configurePluginProject adds plugin project`() {
+        val project = mockk<Project>()
+        val pluginProject = mockk<Project>()
+        val mockBuildType = mockk<com.android.build.gradle.internal.dsl.BuildType>()
+        val mockLogger = mockk<Logger>()
+        every { project.logger } returns mockLogger
+        every { pluginProject.hasProperty("local-engine-repo") } returns false
+        every { pluginProject.hasProperty("android") } returns true
+        every { mockBuildType.name } returns "debug"
+        every { mockBuildType.isDebuggable } returns true
+        every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject
+        every { pluginProject.extensions.create(any(), any<Class<Any>>()) } returns mockk()
+        val captureActionSlot = slot<Action<Project>>()
+        val capturePluginActionSlot = slot<Action<Project>>()
+        every { project.afterEvaluate(any<Action<Project>>()) } returns Unit
+        every { pluginProject.afterEvaluate(any<Action<Project>>()) } returns Unit
+
+        val mockProjectBuildTypes =
+            mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
+        val mockPluginProjectBuildTypes =
+            mockk<NamedDomainObjectContainer<com.android.build.gradle.internal.dsl.BuildType>>()
+        every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes
+        every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes
+        every { mockPluginProjectBuildTypes.addAll(any()) } returns true
+        every { pluginProject.configurations.named(any<String>()) } returns mockk()
+        every { pluginProject.dependencies.add(any(), any()) } returns mockk()
+
+        every {
+            project.extensions
+                .findByType(BaseExtension::class.java)!!
+                .buildTypes
+                .iterator()
+        } returns
+            mutableListOf(
+                mockBuildType
+            ).iterator() andThen
+            mutableListOf( // can't return the same iterator as it is stateful
+                mockBuildType
+            ).iterator()
+        every { project.dependencies.add(any(), any()) } returns mockk()
+        every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+        every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
+
+        FlutterPluginUtils.configurePluginProject(
+            project = project,
+            pluginObject = cameraDependency,
+            engineVersion = exampleEngineVersion
+        )
+
+        verify { project.afterEvaluate(capture(captureActionSlot)) }
+        verify { pluginProject.afterEvaluate(capture(capturePluginActionSlot)) }
+        captureActionSlot.captured.execute(project)
+        capturePluginActionSlot.captured.execute(pluginProject)
+        verify { pluginProject.extensions.create("flutter", FlutterExtension::class.java) }
+        verify {
+            pluginProject.dependencies.add(
+                "debugApi",
+                "io.flutter:flutter_embedding_debug:$exampleEngineVersion"
+            )
+        }
+        verify { project.dependencies.add("debugApi", pluginProject) }
+        verify { mockLogger wasNot called }
+        verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) }
+    }
+
+    // addTaskForJavaVersion
+    @Test
+    fun `addTaskForJavaVersion adds task for Java version`() {
+        val project = mockk<Project>()
+        every { project.tasks.register(any(), any<Action<Task>>()) } returns mockk()
+        val captureSlot = slot<Action<Task>>()
+        FlutterPluginUtils.addTaskForJavaVersion(project)
+        verify { project.tasks.register("javaVersion", capture(captureSlot)) }
+
+        val mockTask = mockk<Task>()
+        every { mockTask.description = any() } returns Unit
+        every { mockTask.doLast(any<Action<Task>>()) } returns mockk()
+        captureSlot.captured.execute(mockTask)
+        verify {
+            mockTask.description = "Print the current java version used by gradle. see: " +
+                "https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html"
+        }
+    }
+
+    // addTaskForPrintBuildVariants
+    @Test
+    fun `addTaskForPrintBuildVariants adds task for printing build variants`() {
+        val project = mockk<Project>()
+        every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockk()
+        every { project.tasks.register(any(), any<Action<Task>>()) } returns mockk()
+        val captureSlot = slot<Action<Task>>()
+
+        FlutterPluginUtils.addTaskForPrintBuildVariants(project)
+
+        verify { project.tasks.register("printBuildVariants", capture(captureSlot)) }
+        val mockTask = mockk<Task>()
+        every { mockTask.description = any() } returns Unit
+        every { mockTask.doLast(any<Action<Task>>()) } returns mockk()
+
+        captureSlot.captured.execute(mockTask)
+
+        verify {
+            mockTask.description = "Prints out all build variants for this Android project"
+        }
+    }
+
+    companion object {
+        val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
+
+        val devDependency: Map<String?, Any?> =
+            mapOf(
+                Pair("name", "grays_fun_dev_dependency"),
+                Pair(
+                    "path",
+                    "/Users/someuser/.pub-cache/hosted/pub.dev/grays_fun_dev_dependency-1.1.1/"
+                ),
+                Pair("native_build", true),
+                Pair("dependencies", emptyList<String>()),
+                Pair("dev_dependency", true)
+            )
+
+        val cameraDependency: Map<String?, Any?> =
+            mapOf(
+                Pair("name", "camera_android_camerax"),
+                Pair(
+                    "path",
+                    "/Users/someuser/.pub-cache/hosted/pub.dev/camera_android_camerax-0.6.14+1/"
+                ),
+                Pair("native_build", true),
+                Pair("dependencies", emptyList<String>()),
+                Pair("dev_dependency", false)
+            )
+
+        val flutterPluginAndroidLifecycleDependency: Map<String?, Any?> =
+            mapOf(
+                Pair("name", "flutter_plugin_android_lifecycle"),
+                Pair(
+                    "path",
+                    "/Users/someuser/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.27/"
+                ),
+                Pair("native_build", true),
+                Pair("dependencies", emptyList<String>()),
+                Pair("dev_dependency", false)
+            )
+
+        val pluginListWithoutDevDependency: List<Map<String?, Any?>> =
+            listOf(
+                cameraDependency,
+                flutterPluginAndroidLifecycleDependency,
+                mapOf(
+                    Pair("name", "in_app_purchase_android"),
+                    Pair(
+                        "path",
+                        "/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
+                    ),
+                    Pair("native_build", true),
+                    Pair("dependencies", emptyList<String>()),
+                    Pair("dev_dependency", false)
+                )
+            )
+
+        val pluginListWithDevDependency: List<Map<String?, Any?>> =
+            listOf(
+                cameraDependency,
+                flutterPluginAndroidLifecycleDependency,
+                devDependency,
+                mapOf(
+                    Pair("name", "in_app_purchase_android"),
+                    Pair(
+                        "path",
+                        "/Users/someuser/.pub-cache/hosted/pub.dev/in_app_purchase_android-0.4.0+1/"
+                    ),
+                    Pair("native_build", true),
+                    Pair("dependencies", emptyList<String>()),
+                    Pair("dev_dependency", false)
+                )
+            )
+    }
 }
diff --git a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
index 02402ed..c203268 100644
--- a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
+++ b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart
@@ -67,6 +67,7 @@
       'apk',
       '--debug',
     ], workingDirectory: exampleAppDir.path);
+    expect(result, const ProcessResultMatcher());
     expect(
       exampleAppDir
           .childDirectory('build')
@@ -103,6 +104,7 @@
       'apk',
       '--debug',
     ], workingDirectory: exampleAppDir.path);
+    expect(result, const ProcessResultMatcher());
     expect(
       exampleAppDir
           .childDirectory('build')
@@ -155,6 +157,7 @@
       'apk',
       '--debug',
     ], workingDirectory: exampleAppDir.path);
+    expect(result, const ProcessResultMatcher());
     expect(
       exampleAppDir
           .childDirectory('build')