Skip to content

Commit 26c78c8

Browse files
reidbakerSydneyBao
authored andcommitted
Migrate deeplink json creation to public AGP api (flutter#173794)
Reviewers please pay special attention to the tests that were added and tests that were removed. If you see a set of functionality that is not covered and should be please say something. I read every test (and TBH also had to edit most of them) but I had gemini's agent mode help and I dont trust there is something I missed. Related to flutter#173651 Newly added tests can be run from `packages/flutter_tools/gradle` with `./gradlew test --tests com.flutter.gradle.tasks.DeepLinkJsonFromManifestTaskTest` ## 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.
1 parent 714202a commit 26c78c8

File tree

8 files changed

+616
-350
lines changed

8 files changed

+616
-350
lines changed

packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ class FlutterPlugin : Plugin<Project> {
704704
validateDeferredComponents = validateDeferredComponentsValue
705705
flavor = flavorValue
706706
}
707-
val compileTask: FlutterTask = compileTaskProvider.get()
707+
val flutterCompileTask: FlutterTask = compileTaskProvider.get()
708708
val libJar: File =
709709
project.file(
710710
project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar")
@@ -716,10 +716,10 @@ class FlutterPlugin : Plugin<Project> {
716716
) {
717717
destinationDirectory.set(libJar.parentFile)
718718
archiveFileName.set(libJar.name)
719-
dependsOn(compileTask)
719+
dependsOn(flutterCompileTask)
720720
targetPlatforms.forEach { targetPlatform ->
721721
val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform]
722-
from("${compileTask.intermediateDir}/$abi") {
722+
from("${flutterCompileTask.intermediateDir}/$abi") {
723723
include("*.so")
724724
// Move `app.so` to `lib/<abi>/libapp.so`
725725
rename { filename: String -> "lib/$abi/lib$filename" }
@@ -749,8 +749,8 @@ class FlutterPlugin : Plugin<Project> {
749749
"copyFlutterAssets${FlutterPluginUtils.capitalize(variant.name)}",
750750
Copy::class.java
751751
) {
752-
dependsOn(compileTask)
753-
with(compileTask.assets)
752+
dependsOn(flutterCompileTask)
753+
with(flutterCompileTask.assets)
754754
filePermissions {
755755
user {
756756
read = true

packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt

Lines changed: 29 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
package com.flutter.gradle
66

7+
import com.android.build.api.artifact.SingleArtifact
8+
import com.android.build.api.variant.AndroidComponentsExtension
79
import com.android.build.gradle.AbstractAppExtension
810
import com.android.build.gradle.BaseExtension
911
import com.android.build.gradle.tasks.ProcessAndroidResources
1012
import com.android.builder.model.BuildType
1113
import com.flutter.gradle.plugins.PluginHandler
14+
import com.flutter.gradle.tasks.DeepLinkJsonFromManifestTask
1215
import groovy.lang.Closure
13-
import groovy.util.Node
1416
import org.gradle.api.GradleException
1517
import org.gradle.api.Project
1618
import org.gradle.api.Task
@@ -24,10 +26,6 @@ import java.util.Properties
2426
* A collection of static utility functions used by the Flutter Gradle Plugin.
2527
*/
2628
object FlutterPluginUtils {
27-
private const val MANIFEST_NAME_KEY = "android:name"
28-
private const val MANIFEST_VALUE_KEY = "android:value"
29-
private const val MANIFEST_VALUE_TRUE = "true"
30-
3129
// Gradle properties. These must correspond to the values used in
3230
// flutter/packages/flutter_tools/lib/src/android/gradle.dart, and therefore it is not
3331
// recommended to use these const values in tests.
@@ -399,6 +397,7 @@ object FlutterPluginUtils {
399397
return project.extensions.findByType(BaseExtension::class.java)!!
400398
}
401399

400+
// Avoid new usages this class is not part of the public AGP DSL.
402401
private fun getAndroidAppExtensionOrNull(project: Project): AbstractAppExtension? =
403402
project.extensions.findByType(AbstractAppExtension::class.java)
404403

@@ -783,199 +782,43 @@ object FlutterPluginUtils {
783782
* Add a task that can be called on Flutter projects that outputs app link related project
784783
* settings into a json file.
785784
* See https://developer.android.com/training/app-links/ for more information about app link.
786-
* The json will be saved in path stored in outputPath parameter.
785+
* The json will be saved in path stored in "outputPath" parameter or in the projects build
786+
* directory with the file deeplink.json if not specified.
787+
*
788+
* See DeepLinkJsonFromManifestTask for the structure of the json.
787789
*
788-
* An example json:
789-
* {
790-
* applicationId: "com.example.app",
791-
* deeplinks: [
792-
* {"scheme":"http", "host":"example.com", "path":".*"},
793-
* {"scheme":"https","host":"example.com","path":".*"}
794-
* ]
795-
* }
796790
* The output file is parsed and used by devtool.
797791
*/
798792
@JvmStatic
799793
@JvmName("addTasksForOutputsAppLinkSettings")
800794
internal fun addTasksForOutputsAppLinkSettings(project: Project) {
801795
// Integration test for AppLinkSettings task defined in
802796
// flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart
803-
val android = getAndroidAppExtensionOrNull(project)
804-
if (android == null) {
805-
project.logger.info("addTasksForOutputsAppLinkSettings called on project without android extension.")
806-
return
807-
}
808-
android.applicationVariants.configureEach {
809-
val variant = this
810-
project.tasks.register("output${capitalize(variant.name)}AppLinkSettings") {
811-
val task: Task = this
812-
task.description =
813-
"stores app links settings for the given build variant of this Android project into a json file."
814-
variant.outputs.configureEach {
815-
// TODO(gmackall): Migrate to AGPs variant api.
816-
// https://github.com/flutter/flutter/issues/166550
817-
@Suppress("DEPRECATION")
818-
val baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput = this
819-
// Deeplinks are defined in AndroidManifest.xml and is only available after
820-
// processResourcesProvider.
821-
dependsOn(findProcessResources(baseVariantOutput))
822-
}
823-
doLast {
824-
// We are configuring the same object before a doLast and in a doLast.
825-
// without a clear reason why. That is not good.
826-
variant.outputs.configureEach {
827-
val appLinkSettings = createAppLinkSettings(variant, this)
828-
File(project.property("outputPath").toString()).writeText(
829-
appLinkSettings.toJson().toString()
797+
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
798+
androidComponents.onVariants { variant ->
799+
val manifestUpdater =
800+
project.tasks.register("output${capitalize(variant.name)}AppLinkSettings", DeepLinkJsonFromManifestTask::class.java) {
801+
namespace.set(variant.namespace)
802+
// Flutter should always use project.layout.buildDirectory.file("deeplink.json")
803+
// instead of relying on passing in a path.
804+
if (project.hasProperty("outputPath")) {
805+
deepLinkJson.set(
806+
File(project.property("outputPath").toString())
830807
)
808+
} else {
809+
deepLinkJson.set(project.layout.buildDirectory.file("deeplink.json"))
831810
}
832811
}
833-
}
834-
}
835-
}
836-
837-
/**
838-
* Extracts app deeplink information from the Android manifest file of a variant then returns
839-
* an AppLinkSettings object.
840-
*
841-
* @param BaseVariantOutput The output of a specific build variant (e.g., debug, release).
842-
* @param variant The application variant being processed.
843-
*/
844-
@Suppress("KDocUnresolvedReference")
845-
private fun createAppLinkSettings(
846-
// TODO(gmackall): Migrate to AGPs variant api.
847-
// https://github.com/flutter/flutter/issues/166550
848-
@Suppress("DEPRECATION") variant: com.android.build.gradle.api.ApplicationVariant,
849-
@Suppress("DEPRECATION") baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput
850-
): AppLinkSettings {
851-
val appLinkSettings = AppLinkSettings(variant.applicationId)
852-
853-
// XmlParser is not namespace aware because it makes querying nodes cumbersome.
854-
// TODO(gmackall): Migrate to AGPs variant api.
855-
// https://github.com/flutter/flutter/issues/166550
856-
@Suppress("DEPRECATION")
857-
val manifest: Node =
858-
groovy.xml
859-
.XmlParser(false, false)
860-
.parse(findProcessResources(baseVariantOutput).manifestFile)
861-
val applicationNode: Node? =
862-
manifest.children().find { node ->
863-
node is Node && node.name() == "application"
864-
} as Node?
865-
if (applicationNode == null) {
866-
return appLinkSettings
867-
}
868-
val activities: List<Node> =
869-
applicationNode.children().filterIsInstance<Node>().filter { item ->
870-
item.name() == "activity"
871-
}
872-
873-
activities.forEach { activity ->
874-
val metaDataItems: List<Node> =
875-
activity.children().filterIsInstance<Node>().filter { metaItem ->
876-
metaItem.name() == "meta-data"
877-
}
878-
metaDataItems.forEach { metaDataItem ->
879-
val nameAttribute: Boolean =
880-
metaDataItem.attribute(MANIFEST_NAME_KEY) == "flutter_deeplinking_enabled"
881-
val valueAttribute: Boolean =
882-
metaDataItem.attribute(MANIFEST_VALUE_KEY) == MANIFEST_VALUE_TRUE
883-
if (nameAttribute && valueAttribute) {
884-
appLinkSettings.deeplinkingFlagEnabled = true
885-
}
886-
}
887-
val intentFilterItems: List<Node> =
888-
activity.children().filterIsInstance<Node>().filter { filterItem ->
889-
filterItem.name() == "intent-filter"
890-
}
891-
intentFilterItems.forEach { appLinkIntent ->
892-
// Print out the host attributes in data tags.
893-
val schemes: MutableSet<String?> = mutableSetOf()
894-
val hosts: MutableSet<String?> = mutableSetOf()
895-
val paths: MutableSet<String?> = mutableSetOf()
896-
val intentFilterCheck = IntentFilterCheck()
897-
if (appLinkIntent.attribute("android:autoVerify") == MANIFEST_VALUE_TRUE) {
898-
intentFilterCheck.hasAutoVerify = true
899-
}
900-
901-
val actionItems: List<Node> =
902-
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
903-
item.name() == "action"
904-
}
905-
// Any action item causes intentFilterCheck to always be true
906-
// and we keep looping instead of exiting out early.
907-
// TODO: Exit out early per intent filter action view.
908-
actionItems.forEach { action ->
909-
if (action.attribute(MANIFEST_NAME_KEY) == "android.intent.action.VIEW") {
910-
intentFilterCheck.hasActionView = true
911-
}
912-
}
913-
val categoryItems: List<Node> =
914-
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
915-
item.name() == "category"
916-
}
917-
categoryItems.forEach { category ->
918-
// TODO: Exit out early per intent filter default category.
919-
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.DEFAULT") {
920-
intentFilterCheck.hasDefaultCategory = true
921-
}
922-
// TODO: Exit out early per intent filter browsable category.
923-
if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.BROWSABLE") {
924-
intentFilterCheck.hasBrowsableCategory =
925-
true
926-
}
927-
}
928-
val dataItems: List<Node> =
929-
appLinkIntent.children().filterIsInstance<Node>().filter { item ->
930-
item.name() == "data"
931-
}
932-
dataItems.forEach { data ->
933-
data.attributes().forEach { entry ->
934-
when (entry.key) {
935-
"android:scheme" -> schemes.add(entry.value.toString())
936-
"android:host" -> hosts.add(entry.value.toString())
937-
// All path patterns add to paths.
938-
"android:pathAdvancedPattern" ->
939-
paths.add(
940-
entry.value.toString()
941-
)
942-
943-
"android:pathPattern" -> paths.add(entry.value.toString())
944-
"android:path" -> paths.add(entry.value.toString())
945-
"android:pathPrefix" -> paths.add(entry.value.toString() + ".*")
946-
"android:pathSuffix" -> paths.add(".*" + entry.value.toString())
947-
}
948-
}
949-
}
950-
if (hosts.isNotEmpty() || paths.isNotEmpty()) {
951-
if (schemes.isEmpty()) {
952-
schemes.add(null)
953-
}
954-
if (hosts.isEmpty()) {
955-
hosts.add(null)
956-
}
957-
if (paths.isEmpty()) {
958-
paths.add(".*")
959-
}
960-
// Sets are not ordered this could produce a bug.
961-
schemes.forEach { scheme ->
962-
hosts.forEach { host ->
963-
paths.forEach { path ->
964-
appLinkSettings.deeplinks.add(
965-
Deeplink(
966-
scheme,
967-
host,
968-
path,
969-
intentFilterCheck
970-
)
971-
)
972-
}
973-
}
974-
}
975-
}
976-
}
812+
// This task does not modify the manifest despite using an api
813+
// designed for modification. The task is responsible for an exact copy of the input
814+
// manifest being used for the output manifest.
815+
variant.artifacts
816+
.use(manifestUpdater)
817+
.wiredWithFiles(
818+
DeepLinkJsonFromManifestTask::manifestFile,
819+
DeepLinkJsonFromManifestTask::updatedManifest
820+
).toTransform(SingleArtifact.MERGED_MANIFEST) // (3) Indicate the artifact and operation type.
977821
}
978-
return appLinkSettings
979822
}
980823
}
981824

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package com.flutter.gradle.tasks
6+
7+
import org.gradle.api.DefaultTask
8+
import org.gradle.api.file.RegularFileProperty
9+
import org.gradle.api.provider.Property
10+
import org.gradle.api.tasks.Input
11+
import org.gradle.api.tasks.InputFile
12+
import org.gradle.api.tasks.OutputFile
13+
import org.gradle.api.tasks.TaskAction
14+
15+
/**
16+
* Create a json file of deeplink settings from an AndroidManifest.
17+
*
18+
* This task does not modify the manifest despite using an api
19+
* designed for modification. The task is responsible for an exact copy of the input
20+
* manifest being used for the output manifest.
21+
*/
22+
abstract class DeepLinkJsonFromManifestTask : DefaultTask() {
23+
// Input property to receive the manifest file
24+
@get:InputFile
25+
abstract val manifestFile: RegularFileProperty
26+
27+
// In the past for this task namespace was the ApplicationId.
28+
@get:Input
29+
abstract val namespace: Property<String>
30+
31+
// Does not need to transform manifest at all but there does not appear to be another dsl
32+
// supported way to depend on the merged manifest.
33+
@get:OutputFile
34+
abstract val updatedManifest: RegularFileProperty
35+
36+
@get:OutputFile
37+
abstract val deepLinkJson: RegularFileProperty
38+
39+
@TaskAction
40+
fun processManifest() {
41+
manifestFile.get().asFile.copyTo(updatedManifest.get().asFile, overwrite = true)
42+
logger.debug("DeepLinkJsonFromManifestTask: Unmodified manifest written.")
43+
44+
DeepLinkJsonFromManifestTaskHelper.createAppLinkSettingsFile(namespace.get(), manifestFile, deepLinkJson)
45+
logger.debug("DeepLinkJsonFromManifestTask: appLinkSettings written to ${deepLinkJson.get().asFile.absolutePath}.")
46+
}
47+
}

0 commit comments

Comments
 (0)