flutter.groovy 72.4 KB
Newer Older
1
/* groovylint-disable LineLength, UnnecessaryGString, UnnecessaryGetter */
2 3 4 5 6
// 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.

import com.android.build.OutputFile
7
import groovy.json.JsonSlurper
8 9
import groovy.json.JsonGenerator
import groovy.xml.QName
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
import java.nio.file.Paths
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.Task
import org.gradle.api.file.CopySpec
import org.gradle.api.file.FileCollection
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.bundling.Jar
import org.gradle.internal.os.OperatingSystem

/**
33 34
 * For apps only. Provides the flutter extension used in the app-level Gradle
 * build file (app/build.gradle or app/build.gradle.kts).
35 36 37 38 39 40 41 42 43
 *
 * The versions specified here should match the values in
 * packages/flutter_tools/lib/src/android/gradle_utils.dart, so when bumping,
 * make sure to update the versions specified there.
 *
 * Learn more about extensions in Gradle:
 *  * https://docs.gradle.org/8.0.2/userguide/custom_plugins.html#sec:getting_input_from_the_build
*/
class FlutterExtension {
44

45
    /** Sets the compileSdkVersion used by default in Flutter app projects. */
46
    public final int compileSdkVersion = 34
47 48

    /** Sets the minSdkVersion used by default in Flutter app projects. */
49
    public  final int minSdkVersion = 21
50

51 52 53 54 55 56
    /**
     * Sets the targetSdkVersion used by default in Flutter app projects.
     * targetSdkVersion should always be the latest available stable version.
     *
     * See https://developer.android.com/guide/topics/manifest/uses-sdk-element.
     */
57
    public final int targetSdkVersion = 33
58 59 60 61

    /**
     * Sets the ndkVersion used by default in Flutter app projects.
     * Chosen as default version of the AGP version below as found in
62
     * https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp.
63
     */
64
    public final String ndkVersion = "23.1.7779620"
65 66 67

    /**
     * Specifies the relative directory to the Flutter project directory.
68
     * In an app project, this is ../.. since the app's Gradle build file is under android/app.
69
     */
70
    String source
71 72 73

    /** Allows to override the target file. Otherwise, the target is lib/main.dart. */
    String target
74 75

    /** The versionCode that was read from app's local.properties. */
76
    public String flutterVersionCode = null
77 78

    /** The versionName that was read from app's local.properties. */
79
    public String flutterVersionName = null
80 81

    /** Returns flutterVersionCode as an integer with error handling. */
82
    public Integer versionCode() {
83 84 85 86 87 88 89 90 91 92 93 94
        if (flutterVersionCode == null) {
            throw new GradleException("flutterVersionCode must not be null.")
        }

        if (!flutterVersionCode.isNumber()) {
            throw new GradleException("flutterVersionCode must be an integer.")
        }

        return flutterVersionCode.toInteger()
    }

    /** Returns flutterVersionName with error handling. */
95
    public String versionName() {
96 97 98 99 100 101
        if (flutterVersionName == null) {
            throw new GradleException("flutterVersionName must not be null.")
        }

        return flutterVersionName
    }
102

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
}

// This buildscript block supplies dependencies for this file's own import
// declarations above. It exists solely for compatibility with projects that
// have not migrated to declaratively apply the Flutter Gradle Plugin;
// for those that have, FGP's `build.gradle.kts`  takes care of this.
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        // When bumping, also update:
        //  * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/flutter.groovy
        //  * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart
        //  * AGP version in dependencies block in packages/flutter_tools/gradle/build.gradle.kts
119
        classpath("com.android.tools.build:gradle:7.3.0")
120 121 122 123 124
    }
}

/**
 * Some apps don't set default compile options.
125 126
 * Apps can change these values in the app-level Gradle build file
 * (android/app/build.gradle or android/app/build.gradle.kts).
127 128 129 130 131 132 133 134 135 136 137 138
 * This just ensures that default values are set.
 */
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

apply plugin: FlutterPlugin

class FlutterPlugin implements Plugin<Project> {
139 140

    private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com"
141 142

    /** The platforms that can be passed to the `--Ptarget-platform` flag. */
143 144 145 146
    private static final String PLATFORM_ARM32  = "android-arm"
    private static final String PLATFORM_ARM64  = "android-arm64"
    private static final String PLATFORM_X86    = "android-x86"
    private static final String PLATFORM_X86_64 = "android-x64"
147 148

    /** The ABI architectures supported by Flutter. */
149 150 151 152
    private static final String ARCH_ARM32      = "armeabi-v7a"
    private static final String ARCH_ARM64      = "arm64-v8a"
    private static final String ARCH_X86        = "x86"
    private static final String ARCH_X86_64     = "x86_64"
153

154
    private static final String INTERMEDIATES_DIR = "intermediates"
155 156 157 158 159 160 161 162 163 164 165 166 167 168

    /** Maps platforms to ABI architectures. */
    private static final Map PLATFORM_ARCH_MAP = [
        (PLATFORM_ARM32)    : ARCH_ARM32,
        (PLATFORM_ARM64)    : ARCH_ARM64,
        (PLATFORM_X86)      : ARCH_X86,
        (PLATFORM_X86_64)   : ARCH_X86_64,
    ]

    /**
     * The version code that gives each ABI a value.
     * For each APK variant, use the following versions to override the version of the Universal APK.
     * Otherwise, the Play Store will complain that the APK variants have the same version.
     */
169
    private static final Map<String, Integer> ABI_VERSION = [
170 171 172 173 174 175 176 177 178 179 180 181 182
        (ARCH_ARM32)        : 1,
        (ARCH_ARM64)        : 2,
        (ARCH_X86)          : 3,
        (ARCH_X86_64)       : 4,
    ]

    /** When split is enabled, multiple APKs are generated per each ABI. */
    private static final List DEFAULT_PLATFORMS = [
        PLATFORM_ARM32,
        PLATFORM_ARM64,
        PLATFORM_X86_64,
    ]

183 184 185
    private final static String propLocalEngineRepo = "local-engine-repo"
    private final static String propProcessResourcesProvider = "processResourcesProvider"

186 187 188 189 190 191 192 193 194 195 196 197 198
    /**
     * The name prefix for flutter builds. This is used to identify gradle tasks
     * where we expect the flutter tool to provide any error output, and skip the
     * standard Gradle error output in the FlutterEventLogger. If you change this,
     * be sure to change any instances of this string in symbols in the code below
     * to match.
     */
    static final String FLUTTER_BUILD_PREFIX = "flutterBuild"

    private Project project
    private File flutterRoot
    private File flutterExecutable
    private String localEngine
199
    private String localEngineHost
200 201 202
    private String localEngineSrcPath
    private Properties localProperties
    private String engineVersion
203
    private String engineRealm
204 205 206 207

    /**
     * Flutter Docs Website URLs for help messages.
     */
208
    private final String kWebsiteDeploymentAndroidBuildConfig = "https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration"
209 210 211 212 213

    @Override
    void apply(Project project) {
        this.project = project

214
        Project rootProject = project.rootProject
215
        if (isFlutterAppProject()) {
216
            rootProject.tasks.register("generateLockfiles") {
217
                rootProject.subprojects.each { subproject ->
218
                    String gradlew = (OperatingSystem.current().isWindows()) ?
219 220
                        "${rootProject.projectDir}/gradlew.bat" : "${rootProject.projectDir}/gradlew"
                    rootProject.exec {
221 222 223
                        workingDir(rootProject.projectDir)
                        executable(gradlew)
                        args(":${subproject.name}:dependencies", "--write-locks")
224 225 226 227 228
                    }
                }
            }
        }

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
        String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
        if (flutterRootPath == null) {
            throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
        }
        flutterRoot = project.file(flutterRootPath)
        if (!flutterRoot.isDirectory()) {
            throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
        }

        engineVersion = useLocalEngine()
            ? "+" // Match any version since there's only one.
            : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()

        engineRealm = Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.realm").toFile().text.trim()
        if (engineRealm) {
            engineRealm = engineRealm + "/"
        }

247 248 249
        // Configure the Maven repository.
        String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
        String repository = useLocalEngine()
250
            ? project.property(propLocalEngineRepo)
251
            : "$hostedRepository/${engineRealm}download.flutter.io"
252 253 254
        rootProject.allprojects {
            repositories {
                maven {
255
                    url(repository)
256 257 258 259
                }
            }
        }

260 261 262
        FlutterExtension extension = project.extensions.create("flutter", FlutterExtension)
        Properties localProperties = new Properties()
        File localPropertiesFile = rootProject.file("local.properties")
263 264 265 266 267 268
        if (localPropertiesFile.exists()) {
            localPropertiesFile.withReader("UTF-8") { reader ->
                localProperties.load(reader)
            }
        }

269
        Object flutterVersionCode = localProperties.getProperty("flutter.versionCode")
270 271 272 273 274
        if (flutterVersionCode == null) {
            flutterVersionCode = "1"
        }
        extension.flutterVersionCode = flutterVersionCode

275
        Object flutterVersionName = localProperties.getProperty("flutter.versionName")
276 277 278 279 280
        if (flutterVersionName == null) {
            flutterVersionName = "1.0"
        }
        extension.flutterVersionName = flutterVersionName

281 282 283 284 285 286 287 288 289 290
        this.addFlutterTasks(project)

        // By default, assembling APKs generates fat APKs if multiple platforms are passed.
        // Configuring split per ABI allows to generate separate APKs for each abi.
        // This is a noop when building a bundle.
        if (shouldSplitPerAbi()) {
            project.android {
                splits {
                    abi {
                        // Enables building multiple APKs per ABI.
291
                        enable(true)
292 293 294
                        // Resets the list of ABIs that Gradle should create APKs for to none.
                        reset()
                        // Specifies that we do not want to also generate a universal APK that includes all ABIs.
295
                        universalApk(false)
296 297 298 299
                    }
                }
            }
        }
300 301 302
        final String propDeferredComponentNames = "deferred-component-names"
        if (project.hasProperty(propDeferredComponentNames)) {
            String[] componentNames = project.property(propDeferredComponentNames).split(",").collect {":${it}"}
303 304 305 306 307 308 309 310 311 312 313
            project.android {
                dynamicFeatures = componentNames
            }
        }

        getTargetPlatforms().each { targetArch ->
            String abiValue = PLATFORM_ARCH_MAP[targetArch]
            project.android {
                if (shouldSplitPerAbi()) {
                    splits {
                        abi {
314
                            include(abiValue)
315 316 317 318 319 320 321
                        }
                    }
                }
            }
        }

        String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
322
        flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile()
323 324 325 326 327 328 329 330 331

        // Use Kotlin DSL to handle baseApplicationName logic due to Groovy dynamic dispatch bug.
        project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "kotlin", "flutter.gradle.kts")

        String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                "gradle", "flutter_proguard_rules.pro")
        project.android.buildTypes {
            // Add profile build type.
            profile {
332
                initWith(debug)
333 334 335 336 337 338 339 340 341 342 343 344
                if (it.hasProperty("matchingFallbacks")) {
                    matchingFallbacks = ["debug", "release"]
                }
            }
            // TODO(garyq): Shrinking is only false for multi apk split aot builds, where shrinking is not allowed yet.
            // This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove
            // this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see
            // increased app size due to this.
            if (shouldShrinkResources(project)) {
                release {
                    // Enables code shrinking, obfuscation, and optimization for only
                    // your project's release build type.
345
                    minifyEnabled(true)
346 347
                    // Enables resource shrinking, which is performed by the Android Gradle plugin.
                    // The resource shrinker can't be used for libraries.
348
                    shrinkResources(isBuiltAsApp(project))
349 350
                    // Fallback to `android/app/proguard-rules.pro`.
                    // This way, custom Proguard rules can be configured as needed.
351
                    proguardFiles(project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro")
352 353 354 355 356 357
                }
            }
        }

        if (useLocalEngine()) {
            // This is required to pass the local engine to flutter build aot.
358
            String engineOutPath = project.property("local-engine-out")
359 360
            File engineOut = project.file(engineOutPath)
            if (!engineOut.isDirectory()) {
361
                throw new GradleException("local-engine-out must point to a local engine build")
362 363 364
            }
            localEngine = engineOut.name
            localEngineSrcPath = engineOut.parentFile.parent
365

366
            String engineHostOutPath = project.property("local-engine-host-out")
367 368
            File engineHostOut = project.file(engineHostOutPath)
            if (!engineHostOut.isDirectory()) {
369
                throw new GradleException("local-engine-host-out must point to a local engine host build")
370
            }
371
            localEngineHost = engineHostOut.name
372
        }
373
        project.android.buildTypes.all(this.&addFlutterDependencies)
374 375 376
    }

    private static Boolean shouldShrinkResources(Project project) {
377 378 379
        final String propShrink = "shrink"
        if (project.hasProperty(propShrink)) {
            return project.property(propShrink).toBoolean()
380 381 382 383
        }
        return true
    }

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
    private static String toCamelCase(List<String> parts) {
        if (parts.empty) {
            return ""
        }
        return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}"
    }

    private static Properties readPropertiesIfExist(File propertiesFile) {
        Properties result = new Properties()
        if (propertiesFile.exists()) {
            propertiesFile.withReader("UTF-8") { reader -> result.load(reader) }
        }
        return result
    }

    private static Boolean isBuiltAsApp(Project project) {
        // Projects are built as applications when the they use the `com.android.application`
        // plugin.
        return project.plugins.hasPlugin("com.android.application")
    }

    private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) {
        String configuration
        // `compile` dependencies are now `api` dependencies.
        if (project.getConfigurations().findByName("api")) {
            configuration = "${variantName}Api"
        } else {
            configuration = "${variantName}Compile"
        }
        project.dependencies.add(configuration, dependency, config)
    }

    // 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.
    //
    // See https://developer.android.com/training/app-links/ for more information about app link.
    //
    // The json will be saved in path stored in outputPath parameter.
    //
    // An example json:
    // {
    //   applicationId: "com.example.app",
    //   deeplinks: [
    //     {"scheme":"http", "host":"example.com", "path":".*"},
    //     {"scheme":"https","host":"example.com","path":".*"}
    //   ]
    // }
    //
    // The output file is parsed and used by devtool.
    private static void addTasksForOutputsAppLinkSettings(Project project) {
        project.android.applicationVariants.all { variant ->
            // Warning: The name of this task is used by AndroidBuilder.outputsAppLinkSettings
            project.tasks.register("output${variant.name.capitalize()}AppLinkSettings") {
                description "stores app links settings for the given build variant of this Android project into a json file."
                variant.outputs.all { output ->
                    // Deeplinks are defined in AndroidManifest.xml and is only available after
                    // `processResourcesProvider`.
                    Object processResources = output.hasProperty(propProcessResourcesProvider) ?
                            output.processResourcesProvider.get() : output.processResources
                    dependsOn processResources.name
                }
                doLast {
                    AppLinkSettings appLinkSettings = new AppLinkSettings()
                    appLinkSettings.applicationId = variant.applicationId
                    appLinkSettings.deeplinks = [] as Set<Deeplink>
                    variant.outputs.all { output ->
                        Object processResources = output.hasProperty(propProcessResourcesProvider) ?
                                output.processResourcesProvider.get() : output.processResources
                        def manifest = new XmlParser().parse(processResources.manifestFile)
                        manifest.application.activity.each { activity ->
                            activity."meta-data".each { metadata ->
                                boolean nameAttribute = metadata.attributes().find { it.key == 'android:name' }?.value == 'flutter_deeplinking_enabled'
                                boolean valueAttribute = metadata.attributes().find { it.key == 'android:value' }?.value == 'true'
                                if (nameAttribute && valueAttribute) {
                                    appLinkSettings.deeplinkingFlagEnabled = true
                                }
                            }
                            activity."intent-filter".each { appLinkIntent ->
                                // Print out the host attributes in data tags.
                                Set<String> schemes = [] as Set<String>
                                Set<String> hosts = [] as Set<String>
                                Set<String> paths = [] as Set<String>
                                IntentFilterCheck intentFilterCheck = new IntentFilterCheck()

                                if (appLinkIntent.attributes().find { it.key == 'android:autoVerify' }?.value == 'true') {
                                    intentFilterCheck.hasAutoVerify = true
                            }
                                appLinkIntent.'action'.each { action ->
                                    if (action.attributes().find { it.key == 'android:name' }?.value == 'android.intent.action.VIEW') {
                                        intentFilterCheck.hasActionView = true
                                }
                        }
                                appLinkIntent.'category'.each { category ->
                                    if (category.attributes().find { it.key == 'android:name' }?.value == 'android.intent.category.DEFAULT') {
                                        intentFilterCheck.hasDefaultCategory = true
                                }
                                    if (category.attributes().find { it.key == 'android:name' }?.value == 'android.intent.category.BROWSABLE') {
                                        intentFilterCheck.hasBrowsableCategory = true
                    }
                }
                                appLinkIntent.data.each { data ->
                                    data.attributes().each { entry ->
                                        if (entry.key instanceof QName) {
                                            switch (entry.key.getLocalPart()) {
                                                case "scheme":
                                                    schemes.add(entry.value)
                                                    break
                                                case "host":
                                                    hosts.add(entry.value)
                                                    break
                                                case "pathAdvancedPattern":
                                                case "pathPattern":
                                                case "path":
                                                    paths.add(entry.value)
                                                    break
                                                case "pathPrefix":
                                                    paths.add("${entry.value}.*")
                                                    break
                                                case "pathSuffix":
                                                    paths.add(".*${entry.value}")
                                                    break
                                            }
                                        }
                                    }
                                }
                                schemes.each { scheme ->
                                    hosts.each { host ->
                                        if (!paths) {
                                            appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: ".*", intentFilterCheck: intentFilterCheck))
                                        } else {
                                            paths.each { path ->
                                                appLinkSettings.deeplinks.add(new Deeplink(scheme: scheme, host: host, path: path, intentFilterCheck: intentFilterCheck))
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    JsonGenerator generator = new JsonGenerator.Options().build()
                    new File(project.getProperty("outputPath")).write(generator.toJson(appLinkSettings))
                }
            }
        }
    }

    /**
     * Returns a Flutter build mode suitable for the specified Android buildType.
     *
     * The BuildType DSL type is not public, and is therefore omitted from the signature.
     *
     * @return "debug", "profile", or "release" (fall-back).
     */
    private static String buildModeFor(buildType) {
        if (buildType.name == "profile") {
            return "profile"
        } else if (buildType.debuggable) {
            return "debug"
        }
        return "release"
    }

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
    /**
     * Adds the dependencies required by the Flutter project.
     * This includes:
     *    1. The embedding
     *    2. libflutter.so
     */
    void addFlutterDependencies(buildType) {
        String flutterBuildMode = buildModeFor(buildType)
        if (!supportsBuildMode(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
        // plugins.
        // 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.
601
        if (!isFlutterAppProject() || getPluginList().size() == 0) {
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
            addApiDependencies(project, buildType.name,
                    "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
        }
        List<String> platforms = getTargetPlatforms().collect()
        // Debug mode includes x86 and x64, which are commonly used in emulators.
        if (flutterBuildMode == "debug" && !useLocalEngine()) {
            platforms.add("android-x86")
            platforms.add("android-x64")
        }
        platforms.each { platform ->
            String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
            // Add the `libflutter.so` dependency.
            addApiDependencies(project, buildType.name,
                    "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
        }
    }

    /**
     * Configures the Flutter plugin dependencies.
     *
     * The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
623
     * the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location.
624 625
     * Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject.
     */
626 627 628
    private void configurePlugins() {
        getPluginList().each(this.&configurePluginProject)
        getPluginDependencies().each(this.&configurePluginDependencies)
629 630 631
    }

    /** Adds the plugin project dependency to the app project. */
632 633
    private void configurePluginProject(String pluginName, String _) {
        Project pluginProject = project.rootProject.findProject(":$pluginName")
634
        if (pluginProject == null) {
635
            project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
636 637 638 639
            return
        }
        // Add plugin dependency to the app project.
        project.dependencies {
640
            api(pluginProject)
641 642 643 644 645 646 647 648 649
        }
        Closure addEmbeddingDependencyToPlugin = { buildType ->
            String flutterBuildMode = 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 (!supportsBuildMode(flutterBuildMode)) {
                return
            }
650
            if (!pluginProject.hasProperty("android")) {
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
                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.
            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) {
674
                project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
675 676 677
                project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
            }

678
            project.android.buildTypes.all(addEmbeddingDependencyToPlugin)
679 680 681 682 683 684 685 686 687 688 689
        }
    }

    /**
     * Compares semantic versions ignoring labels.
     *
     * If the versions are equal (ignoring labels), returns one of the two strings arbitrarily.
     *
     * If minor or patch are omitted (non-conformant to semantic versioning), they are considered zero.
     * If the provided versions in both are equal, the longest version string is returned.
     * For example, "2.8.0" vs "2.8" will always consider "2.8.0" to be the most recent version.
690
     * TODO: Remove this or compareVersionStrings. This does not handle strings like "8.6-rc-2".
691 692
     */
    static String mostRecentSemanticVersion(String version1, String version2) {
693 694
        List version1Tokenized = version1.tokenize(".")
        List version2Tokenized = version2.tokenize(".")
695 696 697
        int version1numTokens = version1Tokenized.size()
        int version2numTokens = version2Tokenized.size()
        int minNumTokens = Math.min(version1numTokens, version2numTokens)
698
        for (int i = 0; i < minNumTokens; i++) {
699 700
            int num1 = version1Tokenized[i].toInteger()
            int num2 = version2Tokenized[i].toInteger()
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716
            if (num1 > num2) {
                return version1
            }
            if (num2 > num1) {
                return version2
            }
        }
        if (version1numTokens > version2numTokens) {
            return version1
        }
        return version2
    }

    /** Prints error message and fix for any plugin compileSdkVersion or ndkVersion that are higher than the project. */
    private void detectLowCompileSdkVersionOrNdkVersion() {
        project.afterEvaluate {
717 718 719 720 721
            // 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
722 723 724 725 726
            }
            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
727
            int numProcessedPlugins = getPluginList().size()
728

729 730
            getPluginList().each { plugin ->
                Project pluginProject = project.rootProject.findProject(plugin.key)
731
                pluginProject.afterEvaluate {
732
                    // Default to int min if using a preview version to skip the sdk check.
733
                    int pluginCompileSdkVersion = Integer.MIN_VALUE
734 735
                    // Stable versions use ints, legacy preview uses string.
                    if (getCompileSdkFromProject(pluginProject).isInteger()) {
736
                        pluginCompileSdkVersion = getCompileSdkFromProject(pluginProject) as int
737
                    }
738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
                    maxPluginCompileSdkVersion = Math.max(pluginCompileSdkVersion, maxPluginCompileSdkVersion)
                    String pluginNdkVersion = pluginProject.android.ndkVersion ?: ndkVersionIfUnspecified
                    maxPluginNdkVersion = mostRecentSemanticVersion(pluginNdkVersion, maxPluginNdkVersion)

                    numProcessedPlugins--
                    if (numProcessedPlugins == 0) {
                        if (maxPluginCompileSdkVersion > projectCompileSdkVersion) {
                            project.logger.error("One or more plugins require a higher Android SDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n  compileSdkVersion ${maxPluginCompileSdkVersion}\n  ...\n}\n")
                        }
                        if (maxPluginNdkVersion != projectNdkVersion) {
                            project.logger.error("One or more plugins require a higher Android NDK version.\nFix this issue by adding the following to ${project.projectDir}${File.separator}build.gradle:\nandroid {\n  ndkVersion \"${maxPluginNdkVersion}\"\n  ...\n}\n")
                        }
                    }
                }
            }
        }
    }

756 757 758 759 760
    /**
     * Returns the portion of the compileSdkVersion string that corresponds to either the numeric
     * or string version.
     */
    private String getCompileSdkFromProject(Project gradleProject) {
761
        return gradleProject.android.compileSdkVersion.substring(8)
762 763
    }

764 765 766 767 768 769 770 771
    /**
     * Returns `true` if the given path contains an `android/build.gradle` file.
     */
    private Boolean doesSupportAndroidPlatform(String path) {
        File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
        return editableAndroidProject.exists()
    }

772 773 774 775 776
    /**
     * 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.
     */
777 778 779 780 781
    private void configurePluginDependencies(Object dependencyObject) {
        assert(dependencyObject.name instanceof String)
        Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
        if (pluginProject == null ||
            !doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
782 783
            return
        }
784 785 786
        assert(dependencyObject.dependencies instanceof List)
        dependencyObject.dependencies.each { pluginDependencyName ->
            assert(pluginDependencyName instanceof String)
787 788 789 790
            if (pluginDependencyName.empty) {
                return
            }
            Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
791 792
            if (dependencyProject == null ||
                !doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
793 794 795 796 797
                return
            }
            // Wait for the Android plugin to load and add the dependency to the plugin project.
            pluginProject.afterEvaluate {
                pluginProject.dependencies {
798
                    implementation(dependencyProject)
799 800 801 802 803
                }
            }
        }
    }

804 805 806 807 808 809 810 811 812 813 814 815 816
    private Properties getPluginList() {
        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
        Properties allPlugins = readPropertiesIfExist(pluginsFile)
        Properties androidPlugins = new Properties()
        allPlugins.each { name, path ->
            if (doesSupportAndroidPlatform(path)) {
                androidPlugins.setProperty(name, path)
            }
        // TODO(amirh): log an error if this plugin was specified to be an Android
        // plugin according to the new schema, and was missing a build.gradle file.
        // https://github.com/flutter/flutter/issues/40784
        }
        return androidPlugins
817 818 819
    }

    /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849
    private List getPluginDependencies() {
        // Consider a `.flutter-plugins-dependencies` file with the following content:
        // {
        //     "dependencyGraph": [
        //       {
        //         "name": "plugin-a",
        //         "dependencies": ["plugin-b","plugin-c"]
        //       },
        //       {
        //         "name": "plugin-b",
        //         "dependencies": ["plugin-c"]
        //       },
        //       {
        //         "name": "plugin-c",
        //         "dependencies": []'
        //       }
        //     ]
        //  }
        //
        // This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
        // `plugin-b` depends on `plugin-c`.
        // `plugin-c` doesn't depend on anything.
        File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
        if (pluginsDependencyFile.exists()) {
            def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
            assert(object instanceof Map)
            assert(object.dependencyGraph instanceof List)
            return object.dependencyGraph
        }
        return []
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
    }

    private String resolveProperty(String name, String defaultValue) {
        if (localProperties == null) {
            localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
        }
        String result
        if (project.hasProperty(name)) {
            result = project.property(name)
        }
        if (result == null) {
            result = localProperties.getProperty(name)
        }
        if (result == null) {
            result = defaultValue
        }
        return result
    }

    private List<String> getTargetPlatforms() {
870 871
        final String propTargetPlatform = "target-platform"
        if (!project.hasProperty(propTargetPlatform)) {
872 873
            return DEFAULT_PLATFORMS
        }
874
        return project.property(propTargetPlatform).split(",").collect {
875 876 877 878 879 880 881 882
            if (!PLATFORM_ARCH_MAP[it]) {
                throw new GradleException("Invalid platform: $it.")
            }
            return it
        }
    }

    private Boolean shouldSplitPerAbi() {
883
        return project.findProperty("split-per-abi")?.toBoolean() ?: false
884 885 886
    }

    private Boolean useLocalEngine() {
887
        return project.hasProperty(propLocalEngineRepo)
888 889 890
    }

    private Boolean isVerbose() {
891
        return project.findProperty("verbose")?.toBoolean() ?: false
892 893 894 895
    }

    /** Whether to build the debug app in "fast-start" mode. */
    private Boolean isFastStart() {
896
        return project.findProperty("fast-start")?.toBoolean() ?: false
897 898 899 900 901 902 903 904 905
    }

    /**
     * Returns true if the build mode is supported by the current call to Gradle.
     * This only relevant when using a local engine. Because the engine
     * is built for a specific mode, the call to Gradle must match that mode.
     */
    private Boolean supportsBuildMode(String flutterBuildMode) {
        if (!useLocalEngine()) {
906
            return true
907
        }
908 909
        final String propLocalEngineBuildMode = "local-engine-build-mode"
        assert(project.hasProperty(propLocalEngineBuildMode))
910 911
        // Don't configure dependencies for a build mode that the local engine
        // doesn't support.
912
        return project.property(propLocalEngineBuildMode) == flutterBuildMode
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
    }

    /**
     * Gets the directory that contains the Flutter source code.
     * This is the directory containing the `android/` directory.
     */
    private File getFlutterSourceDirectory() {
        if (project.flutter.source == null) {
            throw new GradleException("Must provide Flutter source directory")
        }
        return project.file(project.flutter.source)
    }

    /**
     * Gets the target file. This is typically `lib/main.dart`.
     */
    private String getFlutterTarget() {
        String target = project.flutter.target
        if (target == null) {
932
            target = "lib/main.dart"
933
        }
934 935 936
        final String propTarget = "target"
        if (project.hasProperty(propTarget)) {
            target = project.property(propTarget)
937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
        }
        return target
    }

    // TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560
    /**
     * In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`.
     * When building APKs, this causes an issue where building release requires the debug JAR,
     * but Gradle won't build debug.
     *
     * To workaround this issue, only configure the JAR task that is required given the task
     * from the command line.
     *
     * The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the
     * time of adding this code. Once released, this can be removed. However, after updating to
952
     * AGP/Gradle 7.2.0/7.5, removing this hack still causes build failures. Further
953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970
     * investigation necessary to remove this.
     *
     * Tested cases:
     * * `./gradlew assembleRelease`
     * * `./gradlew app:assembleRelease.`
     * * `./gradlew assemble{flavorName}Release`
     * * `./gradlew app:assemble{flavorName}Release`
     * * `./gradlew assemble.`
     * * `./gradlew app:assemble.`
     * * `./gradlew bundle.`
     * * `./gradlew bundleRelease.`
     * * `./gradlew app:bundleRelease.`
     *
     * Related issues:
     * https://issuetracker.google.com/issues/158060799
     * https://issuetracker.google.com/issues/158753935
     */
    private boolean shouldConfigureFlutterTask(Task assembleTask) {
971
        List<String> cliTasksNames = project.gradle.startParameter.taskNames
972 973 974
        if (cliTasksNames.size() != 1 || !cliTasksNames.first().contains("assemble")) {
            return true
        }
975
        String taskName = cliTasksNames.first().split(":").last()
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007
        if (taskName == "assemble") {
            return true
        }
        if (taskName == assembleTask.name) {
            return true
        }
        if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) {
            return true
        }
        if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) {
            return true
        }
        if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) {
            return true
        }
        return false
    }

    private Task getAssembleTask(variant) {
        // `assemble` became `assembleProvider` in AGP 3.3.0.
        return variant.hasProperty("assembleProvider") ? variant.assembleProvider.get() : variant.assemble
    }

    private boolean isFlutterAppProject() {
        return project.android.hasProperty("applicationVariants")
    }

    private void addFlutterTasks(Project project) {
        if (project.state.failure) {
            return
        }
        String[] fileSystemRootsValue = null
1008 1009 1010
        final String propFileSystemRoots = "filesystem-roots"
        if (project.hasProperty(propFileSystemRoots)) {
            fileSystemRootsValue = project.property(propFileSystemRoots).split("\\|")
1011 1012
        }
        String fileSystemSchemeValue = null
1013 1014 1015
        final String propFileSystemScheme = "filesystem-scheme"
        if (project.hasProperty(propFileSystemScheme)) {
            fileSystemSchemeValue = project.property(propFileSystemScheme)
1016 1017
        }
        Boolean trackWidgetCreationValue = true
1018 1019 1020
        final String propTrackWidgetCreation = "track-widget-creation"
        if (project.hasProperty(propTrackWidgetCreation)) {
            trackWidgetCreationValue = project.property(propTrackWidgetCreation).toBoolean()
1021
        }
1022
        String frontendServerStarterPathValue = null
1023 1024 1025
        final String propFrontendServerStarterPath = "frontend-server-starter-path"
        if (project.hasProperty(propFrontendServerStarterPath)) {
            frontendServerStarterPathValue = project.property(propFrontendServerStarterPath)
1026
        }
1027
        String extraFrontEndOptionsValue = null
1028 1029 1030
        final String propExtraFrontEndOptions = "extra-front-end-options"
        if (project.hasProperty(propExtraFrontEndOptions)) {
            extraFrontEndOptionsValue = project.property(propExtraFrontEndOptions)
1031 1032
        }
        String extraGenSnapshotOptionsValue = null
1033 1034 1035
        final String propExtraGenSnapshotOptions = "extra-gen-snapshot-options"
        if (project.hasProperty(propExtraGenSnapshotOptions)) {
            extraGenSnapshotOptionsValue = project.property(propExtraGenSnapshotOptions)
1036 1037
        }
        String splitDebugInfoValue = null
1038 1039 1040
        final String propSplitDebugInfo = "split-debug-info"
        if (project.hasProperty(propSplitDebugInfo)) {
            splitDebugInfoValue = project.property(propSplitDebugInfo)
1041 1042
        }
        Boolean dartObfuscationValue = false
1043 1044 1045
        final String propDartObfuscation = "dart-obfuscation"
        if (project.hasProperty(propDartObfuscation)) {
            dartObfuscationValue = project.property(propDartObfuscation).toBoolean()
1046 1047
        }
        Boolean treeShakeIconsOptionsValue = false
1048 1049 1050
        final String propTreeShakeIcons = "tree-shake-icons"
        if (project.hasProperty(propTreeShakeIcons)) {
            treeShakeIconsOptionsValue = project.property(propTreeShakeIcons).toBoolean()
1051 1052
        }
        String dartDefinesValue = null
1053 1054 1055
        final String propDartDefines = "dart-defines"
        if (project.hasProperty(propDartDefines)) {
            dartDefinesValue = project.property(propDartDefines)
1056
        }
1057 1058 1059 1060
        String bundleSkSLPathValue
        final String propBundleSkslPath = "bundle-sksl-path"
        if (project.hasProperty(propBundleSkslPath)) {
            bundleSkSLPathValue = project.property(propBundleSkslPath)
1061
        }
1062 1063 1064 1065
        String performanceMeasurementFileValue
        final String propPerformanceMesaurementFile = "performance-measurement-file"
        if (project.hasProperty(propPerformanceMesaurementFile)) {
            performanceMeasurementFileValue = project.property(propPerformanceMesaurementFile)
1066
        }
1067 1068 1069 1070
        String codeSizeDirectoryValue
        final String propCodeSizeDirectory = "code-size-directory"
        if (project.hasProperty(propCodeSizeDirectory)) {
            codeSizeDirectoryValue = project.property(propCodeSizeDirectory)
1071 1072
        }
        Boolean deferredComponentsValue = false
1073 1074 1075
        final String propDeferredComponents = "deferred-components"
        if (project.hasProperty(propDeferredComponents)) {
            deferredComponentsValue = project.property(propDeferredComponents).toBoolean()
1076 1077
        }
        Boolean validateDeferredComponentsValue = true
1078 1079 1080
        final String propValidateDeferredComponents = "validate-deferred-components"
        if (project.hasProperty(propValidateDeferredComponents)) {
            validateDeferredComponentsValue = project.property(propValidateDeferredComponents).toBoolean()
1081 1082
        }
        addTaskForJavaVersion(project)
1083
        if (isFlutterAppProject()) {
1084
            addTaskForPrintBuildVariants(project)
1085
            addTasksForOutputsAppLinkSettings(project)
1086
        }
1087
        List<String> targetPlatforms = getTargetPlatforms()
1088 1089 1090 1091 1092 1093 1094
        def addFlutterDeps = { variant ->
            if (shouldSplitPerAbi()) {
                variant.outputs.each { output ->
                    // Assigns the new version code to versionCodeOverride, which changes the version code
                    // for only the output APK, not for the variant itself. Skipping this step simply
                    // causes Gradle to use the value of variant.versionCode for the APK.
                    // For more, see https://developer.android.com/studio/build/configure-apk-splits
1095
                    int abiVersionCode = ABI_VERSION.get(output.getFilter(OutputFile.ABI))
1096 1097 1098 1099 1100 1101
                    if (abiVersionCode != null) {
                        output.versionCodeOverride =
                            abiVersionCode * 1000 + variant.versionCode
                    }
                }
            }
1102
            // Build an AAR when this property is defined.
1103
            boolean isBuildingAar = project.hasProperty("is-plugin")
1104 1105 1106 1107 1108 1109
            // In add to app scenarios, a Gradle project contains a `:flutter` and `:app` project.
            // `:flutter` is used as a subproject when these tasks exists and the build isn't building an AAR.
            Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
            Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
            boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar

1110
            String variantBuildMode = buildModeFor(variant.buildType)
1111
            String flavorValue = variant.getFlavorName()
1112 1113 1114 1115 1116 1117 1118 1119
            String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
            // Be careful when configuring task below, Groovy has bizarre
            // scoping rules: writing `verbose isVerbose()` means calling
            // `isVerbose` on the task itself - which would return `verbose`
            // original value. You either need to hoist the value
            // into a separate variable `verbose verboseValue` or prefix with
            // `this` (`verbose this.isVerbose()`).
            FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
                flutterRoot(this.flutterRoot)
                flutterExecutable(this.flutterExecutable)
                buildMode(variantBuildMode)
                minSdkVersion(variant.mergedFlavor.minSdkVersion.apiLevel)
                localEngine(this.localEngine)
                localEngineHost(this.localEngineHost)
                localEngineSrcPath(this.localEngineSrcPath)
                targetPath(getFlutterTarget())
                verbose(this.isVerbose())
                fastStart(this.isFastStart())
                fileSystemRoots(fileSystemRootsValue)
                fileSystemScheme(fileSystemSchemeValue)
                trackWidgetCreation(trackWidgetCreationValue)
1133
                targetPlatformValues = targetPlatforms
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
                sourceDir(getFlutterSourceDirectory())
                intermediateDir(project.file("${project.buildDir}/$INTERMEDIATES_DIR/flutter/${variant.name}/"))
                frontendServerStarterPath(frontendServerStarterPathValue)
                extraFrontEndOptions(extraFrontEndOptionsValue)
                extraGenSnapshotOptions(extraGenSnapshotOptionsValue)
                splitDebugInfo(splitDebugInfoValue)
                treeShakeIcons(treeShakeIconsOptionsValue)
                dartObfuscation(dartObfuscationValue)
                dartDefines(dartDefinesValue)
                bundleSkSLPath(bundleSkSLPathValue)
                performanceMeasurementFile(performanceMeasurementFileValue)
                codeSizeDirectory(codeSizeDirectoryValue)
                deferredComponents(deferredComponentsValue)
                validateDeferredComponents(validateDeferredComponentsValue)
                flavor(flavorValue)
1149 1150
            }
            File libJar = project.file("${project.buildDir}/$INTERMEDIATES_DIR/flutter/${variant.name}/libs.jar")
1151
            Task packJniLibsTask = project.tasks.create(name: "packJniLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
                destinationDirectory = libJar.parentFile
                archiveFileName = libJar.name
                dependsOn compileTask
                targetPlatforms.each { targetPlatform ->
                    String abi = PLATFORM_ARCH_MAP[targetPlatform]
                    from("${compileTask.intermediateDir}/${abi}") {
                        include "*.so"
                        // Move `app.so` to `lib/<abi>/libapp.so`
                        rename { String filename ->
                            return "lib/${abi}/lib${filename}"
                        }
                    }
1164 1165
                    // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble.
                    // The `$project.buildDir` is '.android/Flutter/build/' instead of 'build/'.
1166 1167
                    String buildDir = "${getFlutterSourceDirectory()}/build"
                    String nativeAssetsDir = "${buildDir}/native_assets/android/jniLibs/lib"
1168 1169 1170 1171 1172 1173
                    from("${nativeAssetsDir}/${abi}") {
                        include "*.so"
                        rename { String filename ->
                            return "lib/${abi}/${filename}"
                        }
                    }
1174 1175 1176
                }
            }
            addApiDependencies(project, variant.name, project.files {
1177
                packJniLibsTask
1178 1179 1180 1181 1182
            })
            Task copyFlutterAssetsTask = project.tasks.create(
                name: "copyFlutterAssets${variant.name.capitalize()}",
                type: Copy,
            ) {
1183 1184
                dependsOn(compileTask)
                with(compileTask.assets)
1185
                String currentGradleVersion = project.getGradle().getGradleVersion()
1186 1187 1188

                // See https://docs.gradle.org/current/javadoc/org/gradle/api/file/ConfigurableFilePermissions.html
                // See https://github.com/flutter/flutter/pull/50047
1189
                if (compareVersionStrings(currentGradleVersion, "8.3") >= 0) {
1190 1191 1192 1193 1194 1195 1196 1197 1198
                    filePermissions {
                        user {
                            read = true
                            write = true
                        }
                    }
                } else {
                    // See https://docs.gradle.org/8.2/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:fileMode
                    // See https://github.com/flutter/flutter/pull/50047
1199
                    fileMode(0644)
1200
                }
1201
                if (isUsedAsSubproject) {
1202 1203 1204
                    dependsOn(packageAssets)
                    dependsOn(cleanPackageAssets)
                    into(packageAssets.outputDir)
1205 1206 1207 1208 1209
                    return
                }
                // `variant.mergeAssets` will be removed at the end of 2019.
                def mergeAssets = variant.hasProperty("mergeAssetsProvider") ?
                    variant.mergeAssetsProvider.get() : variant.mergeAssets
1210 1211
                dependsOn(mergeAssets)
                dependsOn("clean${mergeAssets.name.capitalize()}")
1212
                mergeAssets.mustRunAfter("clean${mergeAssets.name.capitalize()}")
1213
                into(mergeAssets.outputDir)
1214 1215 1216
            }
            if (!isUsedAsSubproject) {
                def variantOutput = variant.outputs.first()
1217
                def processResources = variantOutput.hasProperty(propProcessResourcesProvider) ?
1218 1219 1220
                    variantOutput.processResourcesProvider.get() : variantOutput.processResources
                processResources.dependsOn(copyFlutterAssetsTask)
            }
1221 1222 1223
            // The following tasks use the output of copyFlutterAssetsTask,
            // so it's necessary to declare it as an dependency since Gradle 8.
            // See https://docs.gradle.org/8.1/userguide/validation_problems.html#implicit_dependency.
1224 1225
            def compressAssetsTask = project.tasks.findByName("compress${variant.name.capitalize()}Assets")
            if (compressAssetsTask) {
1226
                compressAssetsTask.dependsOn(copyFlutterAssetsTask)
1227
            }
1228 1229 1230

            def bundleAarTask = project.tasks.findByName("bundle${variant.name.capitalize()}Aar")
            if (bundleAarTask) {
1231
                bundleAarTask.dependsOn(copyFlutterAssetsTask)
1232 1233
            }

1234 1235 1236 1237 1238 1239 1240
            return copyFlutterAssetsTask
        } // end def addFlutterDeps

        if (isFlutterAppProject()) {
            project.android.applicationVariants.all { variant ->
                Task assembleTask = getAssembleTask(variant)
                if (!shouldConfigureFlutterTask(assembleTask)) {
1241
                    return
1242 1243 1244
                }
                Task copyFlutterAssetsTask = addFlutterDeps(variant)
                def variantOutput = variant.outputs.first()
1245
                def processResources = variantOutput.hasProperty(propProcessResourcesProvider) ?
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277
                    variantOutput.processResourcesProvider.get() : variantOutput.processResources
                processResources.dependsOn(copyFlutterAssetsTask)

                // Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
                // can discover them. By default, this is `<app-dir>/build/app/outputs/flutter-apk/<filename>.apk`.
                //
                // The filename consists of `app<-abi>?<-flavor-name>?-<build-mode>.apk`.
                // Where:
                //   * `abi` can be `armeabi-v7a|arm64-v8a|x86|x86_64` only if the flag `split-per-abi` is set.
                //   * `flavor-name` is the flavor used to build the app in lower case if the assemble task is called.
                //   * `build-mode` can be `release|debug|profile`.
                variant.outputs.all { output ->
                    assembleTask.doLast {
                        // `packageApplication` became `packageApplicationProvider` in AGP 3.3.0.
                        def outputDirectory = variant.hasProperty("packageApplicationProvider")
                            ? variant.packageApplicationProvider.get().outputDirectory
                            : variant.packageApplication.outputDirectory
                        //  `outputDirectory` is a `DirectoryProperty` in AGP 4.1.
                        String outputDirectoryStr = outputDirectory.metaClass.respondsTo(outputDirectory, "get")
                            ? outputDirectory.get()
                            : outputDirectory
                        String filename = "app"
                        String abi = output.getFilter(OutputFile.ABI)
                        if (abi != null && !abi.isEmpty()) {
                            filename += "-${abi}"
                        }
                        if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
                            filename += "-${variant.flavorName.toLowerCase()}"
                        }
                        filename += "-${buildModeFor(variant.buildType)}"
                        project.copy {
                            from new File("$outputDirectoryStr/${output.outputFileName}")
1278
                            into new File("${project.buildDir}/outputs/flutter-apk")
1279 1280 1281 1282 1283 1284
                            rename {
                                return "${filename}.apk"
                            }
                        }
                    }
                }
1285
                // Copy the native assets created by build.dart and placed here by flutter assemble.
1286
                String nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
1287
                project.android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
1288
            }
1289
            configurePlugins()
1290 1291 1292 1293
            detectLowCompileSdkVersionOrNdkVersion()
            return
        }
        // Flutter host module project (Add-to-app).
1294
        String hostAppProjectName = project.rootProject.hasProperty("flutter.hostAppProjectName") ? project.rootProject.property("flutter.hostAppProjectName") : "app"
1295
        Project appProject = project.rootProject.findProject(":${hostAppProjectName}")
1296
        assert(appProject != null) : "Project :${hostAppProjectName} doesn't exist. To customize the host app project name, set `flutter.hostAppProjectName=<project-name>` in gradle.properties."
1297 1298
        // Wait for the host app project configuration.
        appProject.afterEvaluate {
1299
            assert(appProject.android != null)
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312
            project.android.libraryVariants.all { libraryVariant ->
                Task copyFlutterAssetsTask
                appProject.android.applicationVariants.all { appProjectVariant ->
                    Task appAssembleTask = getAssembleTask(appProjectVariant)
                    if (!shouldConfigureFlutterTask(appAssembleTask)) {
                        return
                    }
                    // Find a compatible application variant in the host app.
                    //
                    // For example, consider a host app that defines the following variants:
                    // | ----------------- | ----------------------------- |
                    // |   Build Variant   |   Flutter Equivalent Variant  |
                    // | ----------------- | ----------------------------- |
1313
                    // |   freeRelease     |   release                     |
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335
                    // |   freeDebug       |   debug                       |
                    // |   freeDevelop     |   debug                       |
                    // |   profile         |   profile                     |
                    // | ----------------- | ----------------------------- |
                    //
                    // This mapping is based on the following rules:
                    // 1. If the host app build variant name is `profile` then the equivalent
                    //    Flutter variant is `profile`.
                    // 2. If the host app build variant is debuggable
                    //    (e.g. `buildType.debuggable = true`), then the equivalent Flutter
                    //    variant is `debug`.
                    // 3. Otherwise, the equivalent Flutter variant is `release`.
                    String variantBuildMode = buildModeFor(libraryVariant.buildType)
                    if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) {
                        return
                    }
                    if (copyFlutterAssetsTask == null) {
                        copyFlutterAssetsTask = addFlutterDeps(libraryVariant)
                    }
                    Task mergeAssets = project
                        .tasks
                        .findByPath(":${hostAppProjectName}:merge${appProjectVariant.name.capitalize()}Assets")
1336
                    assert(mergeAssets)
1337 1338 1339 1340
                    mergeAssets.dependsOn(copyFlutterAssetsTask)
                }
            }
        }
1341
        configurePlugins()
1342 1343
        detectLowCompileSdkVersionOrNdkVersion()
    }
1344 1345 1346

    // compareTo implementation of version strings in the format of ints and periods
    // Requires non null objects.
1347
    // Will not crash on RC candidate strings but considers all RC candidates the same version.
1348
    static int compareVersionStrings(String firstString, String secondString) {
1349 1350
        List firstVersion = firstString.tokenize(".")
        List secondVersion = secondString.tokenize(".")
1351

1352
        int commonIndices = Math.min(firstVersion.size(), secondVersion.size())
1353 1354

        for (int i = 0; i < commonIndices; i++) {
1355 1356
            String firstAtIndex = firstVersion[i]
            String secondAtIndex = secondVersion[i]
1357
            int firstInt = 0
1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376
            int secondInt = 0
            try {
                if (firstAtIndex.contains("-")) {
                    // Strip any chars after "-". For example "8.6-rc-2"
                    firstAtIndex = firstAtIndex.substring(0, firstAtIndex.indexOf('-'))
                }
                firstInt = firstAtIndex.toInteger()
            } catch (NumberFormatException nfe) {
                println(nfe)
            }
            try {
                if (firstAtIndex.contains("-")) {
                    // Strip any chars after "-". For example "8.6-rc-2"
                    secondAtIndex = secondAtIndex.substring(0, secondAtIndex.indexOf('-'))
                }
                secondInt = secondAtIndex.toInteger()
            } catch (NumberFormatException nfe) {
                println(nfe)
            }
1377

1378
            if (firstInt != secondInt) {
1379
                // <=> in groovy delegates to compareTo
1380
                return firstInt <=> secondInt
1381 1382 1383 1384 1385 1386
            }
        }

        // If we got this far then all the common indices are identical, so whichever version is longer must be more recent
        return firstVersion.size() <=> secondVersion.size()
    }
1387

1388 1389
}

1390
class AppLinkSettings {
1391

1392 1393
    String applicationId
    Set<Deeplink> deeplinks
1394
    boolean deeplinkingFlagEnabled
1395

1396
}
1397

1398
class IntentFilterCheck {
1399

1400 1401 1402 1403
    boolean hasAutoVerify
    boolean hasActionView
    boolean hasDefaultCategory
    boolean hasBrowsableCategory
1404

1405 1406 1407 1408
}

class Deeplink {
    String scheme, host, path
1409
    IntentFilterCheck intentFilterCheck
1410
    boolean equals(o) {
1411
        if (o == null) {
1412
            throw new NullPointerException()
1413 1414
        }
        if (o.getClass() != getClass()) {
1415
            return false
1416
        }
1417 1418 1419 1420 1421 1422
        return scheme == o.scheme &&
                host == o.host &&
                path == o.path
    }
}

1423
abstract class BaseFlutterTask extends DefaultTask {
1424

1425 1426
    @Internal
    File flutterRoot
1427

1428 1429
    @Internal
    File flutterExecutable
1430

1431 1432
    @Input
    String buildMode
1433

1434 1435
    @Input
    int minSdkVersion
1436

1437 1438
    @Optional @Input
    String localEngine
1439

1440
    @Optional @Input
1441
    String localEngineHost
1442

1443
    @Optional @Input
1444
    String localEngineSrcPath
1445

1446 1447
    @Optional @Input
    Boolean fastStart
1448

1449 1450
    @Input
    String targetPath
1451

1452 1453
    @Optional @Input
    Boolean verbose
1454

1455 1456
    @Optional @Input
    String[] fileSystemRoots
1457

1458 1459
    @Optional @Input
    String fileSystemScheme
1460

1461 1462
    @Input
    Boolean trackWidgetCreation
1463

1464 1465
    @Optional @Input
    List<String> targetPlatformValues
1466

1467 1468
    @Internal
    File sourceDir
1469

1470 1471
    @Internal
    File intermediateDir
1472

1473
    @Optional @Input
1474
    String frontendServerStarterPath
1475

1476
    @Optional @Input
1477
    String extraFrontEndOptions
1478

1479 1480
    @Optional @Input
    String extraGenSnapshotOptions
1481

1482 1483
    @Optional @Input
    String splitDebugInfo
1484

1485 1486
    @Optional @Input
    Boolean treeShakeIcons
1487

1488 1489
    @Optional @Input
    Boolean dartObfuscation
1490

1491 1492
    @Optional @Input
    String dartDefines
1493

1494 1495
    @Optional @Input
    String bundleSkSLPath
1496

1497
    @Optional @Input
1498 1499
    String codeSizeDirectory

1500
    @Optional @Input
1501 1502
    String performanceMeasurementFile

1503 1504
    @Optional @Input
    Boolean deferredComponents
1505

1506 1507
    @Optional @Input
    Boolean validateDeferredComponents
1508

1509
    @Optional @Input
1510
    String flavor
1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531

    @OutputFiles
    FileCollection getDependenciesFiles() {
        FileCollection depfiles = project.files()

        // Includes all sources used in the flutter compilation.
        depfiles += project.files("${intermediateDir}/flutter_build.d")
        return depfiles
    }

    void buildBundle() {
        if (!sourceDir.isDirectory()) {
            throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
        }

        intermediateDir.mkdirs()

        // Compute the rule name for flutter assemble. To speed up builds that contain
        // multiple ABIs, the target name is used to communicate which ones are required
        // rather than the TargetPlatform. This allows multiple builds to share the same
        // cache.
1532
        String[] ruleNames
1533 1534 1535 1536 1537 1538 1539 1540
        if (buildMode == "debug") {
            ruleNames = ["debug_android_application"]
        } else if (deferredComponents) {
            ruleNames = targetPlatformValues.collect { "android_aot_deferred_components_bundle_${buildMode}_$it" }
        } else {
            ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
        }
        project.exec {
1541 1542 1543
            logging.captureStandardError(LogLevel.ERROR)
            executable(flutterExecutable.absolutePath)
            workingDir(sourceDir)
1544 1545 1546 1547
            if (localEngine != null) {
                args "--local-engine", localEngine
                args "--local-engine-src-path", localEngineSrcPath
            }
1548 1549 1550
            if (localEngineHost != null) {
                args "--local-engine-host", localEngineHost
            }
1551 1552 1553 1554 1555
            if (verbose) {
                args "--verbose"
            } else {
                args "--quiet"
            }
1556 1557 1558 1559
            args("assemble")
            args("--no-version-check")
            args("--depfile", "${intermediateDir}/flutter_build.d")
            args("--output", "${intermediateDir}")
1560
            if (performanceMeasurementFile != null) {
1561
                args("--performance-measurement-file=${performanceMeasurementFile}")
1562 1563
            }
            if (!fastStart || buildMode != "debug") {
1564
                args("-dTargetFile=${targetPath}")
1565
            } else {
1566
                args("-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}")
1567
            }
1568 1569
            args("-dTargetPlatform=android")
            args("-dBuildMode=${buildMode}")
1570
            if (trackWidgetCreation != null) {
1571
                args("-dTrackWidgetCreation=${trackWidgetCreation}")
1572 1573
            }
            if (splitDebugInfo != null) {
1574
                args("-dSplitDebugInfo=${splitDebugInfo}")
1575 1576
            }
            if (treeShakeIcons == true) {
1577
                args("-dTreeShakeIcons=true")
1578 1579
            }
            if (dartObfuscation == true) {
1580
                args("-dDartObfuscation=true")
1581 1582
            }
            if (dartDefines != null) {
1583
                args("--DartDefines=${dartDefines}")
1584 1585
            }
            if (bundleSkSLPath != null) {
1586
                args("-dBundleSkSLPath=${bundleSkSLPath}")
1587 1588
            }
            if (codeSizeDirectory != null) {
1589
                args("-dCodeSizeDirectory=${codeSizeDirectory}")
1590
            }
1591
            if (flavor != null) {
1592
                args("-dFlavor=${flavor}")
1593
            }
1594
            if (extraGenSnapshotOptions != null) {
1595
                args("--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}")
1596
            }
1597
            if (frontendServerStarterPath != null) {
1598
                args("-dFrontendServerStarterPath=${frontendServerStarterPath}")
1599
            }
1600
            if (extraFrontEndOptions != null) {
1601
                args("--ExtraFrontEndOptions=${extraFrontEndOptions}")
1602
            }
1603 1604 1605
            args("-dAndroidArchs=${targetPlatformValues.join(' ')}")
            args("-dMinSdkVersion=${minSdkVersion}")
            args(ruleNames)
1606 1607
        }
    }
1608

1609 1610 1611
}

class FlutterTask extends BaseFlutterTask {
1612

1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625
    @OutputDirectory
    File getOutputDirectory() {
        return intermediateDir
    }

    @Internal
    String getAssetsDirectory() {
        return "${outputDirectory}/flutter_assets"
    }

    @Internal
    CopySpec getAssets() {
        return project.copySpec {
1626 1627
            from("${intermediateDir}")
            include("flutter_assets/**") // the working dir and its files
1628 1629 1630 1631 1632 1633
        }
    }

    @Internal
    CopySpec getSnapshots() {
        return project.copySpec {
1634
            from("${intermediateDir}")
1635

1636
            if (buildMode == "release" || buildMode == "profile") {
1637
                targetPlatformValues.each {
1638
                    include("${PLATFORM_ARCH_MAP[targetArch]}/app.so")
1639 1640 1641 1642 1643 1644
                }
            }
        }
    }

    FileCollection readDependencies(File dependenciesFile, Boolean inputs) {
1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655
        if (dependenciesFile.exists()) {
            // Dependencies file has Makefile syntax:
            //   <target> <files>: <source> <files> <separated> <by> <non-escaped space>
            String depText = dependenciesFile.text
            // So we split list of files by non-escaped(by backslash) space,
            def matcher = depText.split(": ")[inputs ? 1 : 0] =~ /(\\ |[^\s])+/
            // then we replace all escaped spaces with regular spaces
            def depList = matcher.collect{ it[0].replaceAll("\\\\ ", " ") }
            return project.files(depList)
        }
        return project.files()
1656 1657 1658 1659 1660 1661
    }

    @InputFiles
    FileCollection getSourceFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
1662
            sources += readDependencies(depfile, true)
1663
        }
1664
        return sources + project.files("pubspec.yaml")
1665 1666 1667 1668 1669 1670
    }

    @OutputFiles
    FileCollection getOutputFiles() {
        FileCollection sources = project.files()
        for (File depfile in getDependenciesFiles()) {
1671
            sources += readDependencies(depfile, false)
1672 1673 1674 1675 1676 1677 1678 1679
        }
        return sources
    }

    @TaskAction
    void build() {
        buildBundle()
    }
1680

1681
}