forked from firka/flutter
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.") }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user