Move app link settings task configuration to kotlin (#165819)

- **Inital conversion and single test case**
- **Task registered with name**
- **Test can handle multiple variants**
- **configuration for baseOutput setup**
- **Test passing**
- **formatting**
- **Use constant with upppercase**
- **Replace flutter.groovy version of
addTasksForOutputsAppLinkSettings**

Fixes #164393 

Helpful commands: 
`
JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/
./gradlew test --info --tests
"com.flutter.gradle.FlutterPluginUtilsTest"` from
packages/flutter_tools/gradle
`flutter test
test/integration.shard/android_gradle_outputs_app_link_settings_test.dart`
from packages/flutter_tools

## 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].
- [x] 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.
This commit is contained in:
Reid Baker
2025-03-27 18:53:08 +00:00
committed by GitHub
parent 975a677529
commit fd168e1192
4 changed files with 469 additions and 195 deletions

View File

@@ -5,22 +5,16 @@
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
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType
import com.flutter.gradle.BaseApplicationNameHandler
import com.flutter.gradle.Deeplink
import com.flutter.gradle.DependencyVersionChecker
import com.flutter.gradle.FlutterExtension
import com.flutter.gradle.FlutterPluginConstants
import com.flutter.gradle.FlutterTask
import com.flutter.gradle.FlutterPluginUtils
import com.flutter.gradle.IntentFilterCheck
import com.flutter.gradle.VersionUtils
import groovy.xml.QName
import org.gradle.api.file.Directory
import java.nio.file.Paths
@@ -40,7 +34,6 @@ import org.gradle.internal.os.OperatingSystem
class FlutterPlugin implements Plugin<Project> {
private final static String propLocalEngineRepo = "local-engine-repo"
private final static String propProcessResourcesProvider = "processResourcesProvider"
/**
* The name prefix for flutter builds. This is used to identify gradle tasks
@@ -257,125 +250,6 @@ class FlutterPlugin implements Plugin<Project> {
project.android.buildTypes.all(this.&addFlutterDependencies)
}
// 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) {
AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android")
android.applicationVariants.configureEach { 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.configureEach { 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(variant.applicationId)
variant.outputs.configureEach { output ->
Object processResources = output.hasProperty(propProcessResourcesProvider) ?
output.processResourcesProvider.get() : output.processResources
Node 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
}
}
}
}
if (!hosts.isEmpty() || !paths.isEmpty()) {
if (schemes.isEmpty()) {
schemes.add(null)
}
if (hosts.isEmpty()) {
hosts.add(null)
}
if (paths.isEmpty()) {
paths.add('.*')
}
schemes.each { scheme ->
hosts.each { host ->
paths.each { path ->
appLinkSettings.deeplinks.add(new Deeplink(scheme, host, path, intentFilterCheck))
}
}
}
}
}
}
}
new File(project.getProperty("outputPath")).write(appLinkSettings.toJson().toString())
}
}
}
}
/**
* Adds the dependencies required by the Flutter project.
* This includes:
@@ -575,7 +449,7 @@ class FlutterPlugin implements Plugin<Project> {
FlutterPluginUtils.addTaskForJavaVersion(project)
if (FlutterPluginUtils.isFlutterAppProject(project)) {
FlutterPluginUtils.addTaskForPrintBuildVariants(project)
addTasksForOutputsAppLinkSettings(project)
FlutterPluginUtils.addTasksForOutputsAppLinkSettings(project)
}
List<String> targetPlatforms = FlutterPluginUtils.getTargetPlatforms(project)
def addFlutterDeps = { variant ->
@@ -718,7 +592,7 @@ class FlutterPlugin implements Plugin<Project> {
Task copyFlutterAssetsTask = copyFlutterAssetsTaskProvider.get()
if (!isUsedAsSubproject) {
def variantOutput = variant.outputs.first()
def processResources = variantOutput.hasProperty(propProcessResourcesProvider) ?
def processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
}
@@ -749,7 +623,7 @@ class FlutterPlugin implements Plugin<Project> {
}
Task copyFlutterAssetsTask = addFlutterDeps(variant)
BaseVariantOutput variantOutput = variant.outputs.first()
ProcessAndroidResources processResources = variantOutput.hasProperty(propProcessResourcesProvider) ?
ProcessAndroidResources processResources = variantOutput.hasProperty(FlutterPluginConstants.PROP_PROCESS_RESOURCES_PROVIDER) ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)

View File

@@ -3,6 +3,9 @@ package com.flutter.gradle
// TODO(gmackall): this should be collapsed back into the core FlutterPlugin once the Groovy to
// kotlin conversion is complete.
object FlutterPluginConstants {
// Strings that define project properties
const val PROP_PROCESS_RESOURCES_PROVIDER = "processResourcesProvider"
/** The platforms that can be passed to the `--Ptarget-platform` flag. */
private const val PLATFORM_ARM32 = "android-arm"
private const val PLATFORM_ARM64 = "android-arm64"

View File

@@ -2,8 +2,13 @@ package com.flutter.gradle
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType
import groovy.lang.Closure
import groovy.util.Node
import groovy.util.XmlParser
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
@@ -18,6 +23,10 @@ import java.util.Properties
* A collection of static utility functions used by the Flutter Gradle Plugin.
*/
object FlutterPluginUtils {
private const val MANIFEST_NAME_KEY = "android:name"
private const val MANIFEST_VALUE_KEY = "android:value"
private const val MANIFEST_VALUE_TRUE = "true"
// 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.
@@ -399,6 +408,11 @@ object FlutterPluginUtils {
return project.extensions.findByType(BaseExtension::class.java)!!
}
// TODO: Use find by type and AbstractAppExtension instead. Or delete in favor of getAndroidExtension.
// see https://github.com/flutter/flutter/issues/165882
private fun getAndroidAppExtensionOrNull(project: Project): AbstractAppExtension? =
project.extensions.findByName("android") as? AbstractAppExtension
/**
* 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,
@@ -849,6 +863,204 @@ object FlutterPluginUtils {
}
}
}
private fun findProcessResources(baseVariantOutput: BaseVariantOutput): ProcessAndroidResources =
baseVariantOutput.processResourcesProvider?.get() ?: baseVariantOutput.processResources
/**
* Adds required tasks for the AppLinkSettings feature.
*
* Should only be called if the build target is an app, as opposed to an aar/module.
*
* 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.
*/
@JvmStatic
@JvmName("addTasksForOutputsAppLinkSettings")
internal fun addTasksForOutputsAppLinkSettings(project: Project) {
// Integration test for AppLinkSettings task defined in
// flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart
val android = getAndroidAppExtensionOrNull(project)
if (android == null) {
project.logger.info("addTasksForOutputsAppLinkSettings called on project without android extension.")
return
}
android.applicationVariants.configureEach {
val variant = this
project.tasks.register("output${FlutterPluginUtils.capitalize(variant.name)}AppLinkSettings") {
val task: Task = this
task.description =
"stores app links settings for the given build variant of this Android project into a json file."
variant.outputs.configureEach {
val baseVariantOutput: BaseVariantOutput = this
// Deeplinks are defined in AndroidManifest.xml and is only available after
// processResourcesProvider.
dependsOn(findProcessResources(baseVariantOutput))
}
doLast {
// We are configuring the same object before a doLast and in a doLast.
// without a clear reason why. That is not good.
variant.outputs.configureEach {
val appLinkSettings = createAppLinkSettings(variant, this)
File(project.property("outputPath").toString()).writeText(
appLinkSettings.toJson().toString()
)
}
}
}
}
}
/**
* Extracts app deeplink information from the Android manifest file of a variant then returns
* an AppLinkSettings object.
*
* @param BaseVariantOutput The output of a specific build variant (e.g., debug, release).
* @param variant The application variant being processed.
*/
private fun createAppLinkSettings(
variant: ApplicationVariant,
baseVariantOutput: BaseVariantOutput
): AppLinkSettings {
val appLinkSettings = AppLinkSettings(variant.applicationId)
// TODO https://github.com/flutter/flutter/issues/165881
// Use import groovy.xml.XmlParser instead.
// XmlParser is not namespace aware because it makes querying nodes cumbersome.
val manifest: Node =
XmlParser(false, false).parse(findProcessResources(baseVariantOutput).manifestFile)
// The groovy.xml.XmlParser import would use getProperty like
// manifest.getProperty("application").let { applicationNode -> ...
val applicationNode: Node? =
manifest.children().find { node ->
node is Node && node.name() == "application"
} as Node?
if (applicationNode == null) {
return appLinkSettings
}
val activities: List<Node> =
applicationNode.children().filterIsInstance<Node>().filter { item ->
item.name() == "activity"
}
activities.forEach { activity ->
val metaDataItems: List<Node> =
activity.children().filterIsInstance<Node>().filter { metaItem ->
metaItem.name() == "meta-data"
}
metaDataItems.forEach { metaDataItem ->
val nameAttribute: Boolean =
metaDataItem.attribute(MANIFEST_NAME_KEY) == "flutter_deeplinking_enabled"
val valueAttribute: Boolean =
metaDataItem.attribute(MANIFEST_VALUE_KEY) == MANIFEST_VALUE_TRUE
if (nameAttribute && valueAttribute) {
appLinkSettings.deeplinkingFlagEnabled = true
}
}
val intentFilterItems: List<Node> =
activity.children().filterIsInstance<Node>().filter { filterItem ->
filterItem.name() == "intent-filter"
}
intentFilterItems.forEach { appLinkIntent ->
// Print out the host attributes in data tags.
val schemes: MutableSet<String?> = mutableSetOf()
val hosts: MutableSet<String?> = mutableSetOf()
val paths: MutableSet<String?> = mutableSetOf()
val intentFilterCheck = IntentFilterCheck()
if (appLinkIntent.attribute("android:autoVerify") == MANIFEST_VALUE_TRUE) {
intentFilterCheck.hasAutoVerify = true
}
val actionItems: List<Node> =
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
item.name() == "action"
}
// Any action item causes intentFilterCheck to always be true
// and we keep looping instead of exiting out early.
// TODO: Exit out early per intent filter action view.
actionItems.forEach { action ->
if (action.attribute(MANIFEST_NAME_KEY) == "android.intent.action.VIEW") {
intentFilterCheck.hasActionView = true
}
}
val categoryItems: List<Node> =
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
item.name() == "category"
}
categoryItems.forEach { category ->
// TODO: Exit out early per intent filter default category.
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.DEFAULT") {
intentFilterCheck.hasDefaultCategory = true
}
// TODO: Exit out early per intent filter browsable category.
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.BROWSABLE") {
intentFilterCheck.hasBrowsableCategory =
true
}
}
val dataItems: List<Node> =
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
item.name() == "data"
}
dataItems.forEach { data ->
data.attributes().forEach { entry ->
when (entry.key) {
"android:scheme" -> schemes.add(entry.value.toString())
"android:host" -> hosts.add(entry.value.toString())
// All path patterns add to paths.
"android:pathAdvancedPattern" ->
paths.add(
entry.value.toString()
)
"android:pathPattern" -> paths.add(entry.value.toString())
"android:path" -> paths.add(entry.value.toString())
"android:pathPrefix" -> paths.add(entry.value.toString() + ".*")
"android:pathSuffix" -> paths.add(".*" + entry.value.toString())
}
}
}
if (hosts.isNotEmpty() || paths.isNotEmpty()) {
if (schemes.isEmpty()) {
schemes.add(null)
}
if (hosts.isEmpty()) {
hosts.add(null)
}
if (paths.isEmpty()) {
paths.add(".*")
}
// Sets are not ordered this could produce a bug.
schemes.forEach { scheme ->
hosts.forEach { host ->
paths.forEach { path ->
appLinkSettings.deeplinks.add(
Deeplink(
scheme,
host,
path,
intentFilterCheck
)
)
}
}
}
}
}
}
return appLinkSettings
}
}
private data class PluginVersionPair(

View File

@@ -2,8 +2,11 @@ package com.flutter.gradle
import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariantOutput
import com.android.build.gradle.internal.dsl.CmakeOptions
import com.android.build.gradle.internal.dsl.DefaultConfig
import com.android.build.gradle.tasks.ProcessAndroidResources
import com.android.builder.model.BuildType
import io.mockk.called
import io.mockk.every
@@ -11,6 +14,8 @@ import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.gradle.api.Action
import org.gradle.api.DomainObjectCollection
import org.gradle.api.DomainObjectSet
import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
@@ -18,6 +23,8 @@ 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.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import java.io.File
@@ -31,6 +38,116 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class FlutterPluginUtilsTest {
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)
)
)
val manifestText =
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions do not break parsing -->
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="Flutter Task Helper Test" android:icon="@mipmap/ic_launcher">
<activity android:name="com.example.FlutterActivity1"
android:exported="true"
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.example.FlutterActivity2"
android:exported="false"
android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="poc"
android:host="deeplink.flutter.dev"
android:pathPrefix="some.prefix"
/>
</intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
""".trimIndent()
}
// toCamelCase
@Test
fun `toCamelCase converts a list of strings to camel case`() {
@@ -1109,76 +1226,144 @@ class FlutterPluginUtilsTest {
}
}
companion object {
val exampleEngineVersion = "1.0.0-e0676b47c7550ecdc0f0c4fa759201449b2c5f23"
@Test
fun addTasksForOutputsAppLinkSettingsActual(
@TempDir tempDir: Path
) {
val variants: MutableList<ApplicationVariant> = mutableListOf()
val registerTaskList = mutableListOf<Task>()
val descriptionSlot = slot<String>()
// vars so variables can be overridden below.
var mockLogger = mockk<Logger>()
var variantWithLinks = mockk<ApplicationVariant>()
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 mockProject =
mockk<Project> {
every { logger } returns
mockk {
mockLogger = this
every { info(any()) } returns Unit
every { warn(any()) } returns Unit
}
every { extensions.findByName("android") } returns
mockk<AbstractAppExtension> {
val variant1 =
mockk<ApplicationVariant> {
every { name } returns "one"
every { applicationId } returns "com.example.FlutterActivity1"
}
variants.add(variant1)
mockk<ApplicationVariant> {
variantWithLinks = this
every { name } returns "two"
every { applicationId } returns "com.example.FlutterActivity2"
}
variants.add(variantWithLinks)
// Capture the "action" that needs to be run for each variant.
val actionSlot = slot<Action<ApplicationVariant>>()
every { applicationVariants } returns
mockk<DomainObjectSet<ApplicationVariant>> {
every { configureEach(capture(actionSlot)) } answers {
// Execute the action for each variant.
variants.forEach { variant ->
actionSlot.captured.execute(variant)
}
}
}
}
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 registerTaskSlot = slot<Action<Task>>()
every { tasks } returns
mockk<TaskContainer> {
val registerTaskNameSlot = slot<String>()
every { register(capture(registerTaskNameSlot), capture(registerTaskSlot)) } answers registerAnswer@{
val mockRegisterTask =
mockk<Task> {
every { name } returns registerTaskNameSlot.captured
every { description = capture(descriptionSlot) } returns Unit
every { dependsOn(any<ProcessAndroidResources>()) } returns mockk()
val doLastActionSlot = slot<Action<Task>>()
every { doLast(capture(doLastActionSlot)) } answers doLastAnswer@{
// We need to capture the task as well
doLastActionSlot.captured.execute(mockk())
return@doLastAnswer mockk()
}
}
registerTaskList.add(mockRegisterTask)
registerTaskSlot.captured.execute(mockRegisterTask)
return@registerAnswer mockk()
}
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)
)
every { named(any<String>()) } returns
mockk {
every { configure(any<Action<Task>>()) } returns mockk()
}
}
}
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)
)
)
variants.forEach { variant ->
val testOutputs: DomainObjectCollection<BaseVariantOutput> = mockk<DomainObjectCollection<BaseVariantOutput>>()
val baseVariantSlot = slot<Action<BaseVariantOutput>>()
val baseVariantOutput = mockk<BaseVariantOutput>()
// Create a real file in a temp directory.
val manifest =
tempDir
.resolve("${tempDir.toAbsolutePath()}/AndroidManifest.xml")
.toFile()
manifest.writeText(manifestText)
val mockProcessResourcesProvider = mockk<TaskProvider<ProcessAndroidResources>>()
val mockProcessResources = mockk<ProcessAndroidResources>()
every { mockProcessResourcesProvider.hint(ProcessAndroidResources::class).get() } returns mockProcessResources
every { baseVariantOutput.processResourcesProvider } returns mockProcessResourcesProvider
// Fallback processing.
every { mockProcessResources.manifestFile } returns manifest
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)
)
)
every { testOutputs.configureEach(capture(baseVariantSlot)) } answers {
// Execute the action for each output.
baseVariantSlot.captured.execute(baseVariantOutput)
}
every { variant.outputs } returns testOutputs
}
val outputFile =
tempDir
.resolve("${tempDir.toAbsolutePath()}/app-link-settings-build-variant.json")
.toFile()
every { mockProject.property("outputPath") } returns outputFile
FlutterPluginUtils.addTasksForOutputsAppLinkSettings(mockProject)
verify(exactly = 0) { mockLogger.info(any()) }
assert(descriptionSlot.captured.contains("stores app links settings for the given build variant"))
assertEquals(variants.size, registerTaskList.size)
for (i in 0 until variants.size) {
assertEquals("output${FlutterPluginUtils.capitalize(variants[i].name)}AppLinkSettings", registerTaskList[i].name)
verify(exactly = 1) { registerTaskList[i].dependsOn(any<ProcessAndroidResources>()) }
}
// Output assertions are minimal which ensures code is running but is not exhaustive testing.
// Integration test for more exhaustive behavior is defined in
// flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart
val outputFileText = outputFile.readText()
// Only variant2 since that one has app links.
assertContains(outputFileText, variantWithLinks.applicationId)
// Host.
assertContains(outputFileText, "deeplink.flutter.dev")
// pathPrefix used in variant2 combined with prefix logic.
assertContains(outputFileText, "some.prefix.*")
// Deep linking
assertContains(outputFileText, "deeplinkingFlagEnabled\":true")
}
@Test
fun addTasksForOutputsAppLinkSettingsNoAndroid(
@TempDir tempDir: Path
) {
val mockProject = mockk<Project>()
val mockLogger = mockk<Logger>()
every { mockProject.logger } returns mockLogger
every { mockLogger.info(any()) } returns Unit
every { mockProject.extensions.findByName("android") } returns null
FlutterPluginUtils.addTasksForOutputsAppLinkSettings(mockProject)
verify(exactly = 1) { mockLogger.info("addTasksForOutputsAppLinkSettings called on project without android extension.") }
}
}