Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
import java.io.ByteArrayOutputStream
import java.util.regex.Pattern
import org.gradle.api.attributes.Attribute
import java.lang.RuntimeException

evaluationDependsOn(":wear:watchface")

Expand Down Expand Up @@ -66,9 +68,38 @@ configurations {
isCanBeConsumed = false
isCanBeResolved = true
}

create("watchfaceApkDebug"){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a single configuration that doesn't specify com.android.build.api.attributes.BuildTypeAttr::class.java and we just set that in artifactView at the consumption place. That way you don't accidentally add the project dependency just to a single configuration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also should use register instead of create. Configurations are lazy starting 9.1.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestions! I tried adding build attributes and use artifactView on the consumption side(in the copy task). However, it didn't work for me and gradle keeps telling me "consumer didn't ask for build type attribute and it cannot choose between debugRuntimeElements and releaseRuntimeElements. I am quite confused.

I didn't some research and it seems the reason is "attributes on the artifactView only refine what artifacts are retrieved after the initial variant resolution has occurred. They don't provide the necessary attributes to guide the initial variant selection for the entire configuration"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today I learn. Thanks for sharing!

isCanBeResolved = true
isCanBeConsumed = false

attributes {
attribute(
Attribute.of(com.android.build.api.attributes.BuildTypeAttr::class.java),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use import com.android.build.api.attributes.BuildTypeAttr to make this easier to read.

objects.named(com.android.build.api.attributes.BuildTypeAttr::class.java, "debug")
)
attribute(Attribute.of("artifactType", String::class.java), "apk")
}
}

create("watchfaceApkRelease") {
isCanBeResolved = true
isCanBeConsumed = false

attributes {
attribute(
Attribute.of(com.android.build.api.attributes.BuildTypeAttr::class.java),
objects.named(com.android.build.api.attributes.BuildTypeAttr::class.java, "release")
)
attribute(Attribute.of("artifactType", String::class.java), "apk")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's significant duplication in the creation of watchfaceApkDebug and watchfaceApkRelease configurations. You can reduce this by iterating over the build types. This will make the script more maintainable and less error-prone if more build types are added in the future.

    listOf("debug", "release").forEach { buildType ->
        create("watchfaceApk${buildType.replaceFirstChar { it.uppercase() }}") {
            isCanBeResolved = true
            isCanBeConsumed = false

            attributes {
                attribute(
                    Attribute.of(com.android.build.api.attributes.BuildTypeAttr::class.java),
                    objects.named(com.android.build.api.attributes.BuildTypeAttr::class.java, buildType)
                )
                attribute(Attribute.of("artifactType", String::class.java), "apk")
            }
        }
    }

}

dependencies {
configurations.getByName("watchfaceApkDebug").dependencies.add(project(":wear:watchface"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: here you can use

dependencies {
    "watchfaceApkDebug"(project(":wear:watchface"))
}

this syntax as it is easier to read.

configurations.getByName("watchfaceApkRelease").dependencies.add(project(":wear:watchface"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This dependency declaration can also be simplified to avoid duplication. Using a matching clause is an idiomatic way to apply configuration to a group of configurations.

    configurations.matching { it.name.startsWith("watchfaceApk") }.all { dependencies.add(project(":wear:watchface")) }


implementation(projects.wear.common)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.wear.compose.foundation)
Expand All @@ -93,29 +124,29 @@ dependencies {
androidComponents.onVariants { variant ->
val capsVariant = variant.name.replaceFirstChar { it.uppercase() }

val copyTaskProvider = tasks.register<Copy>("copyWatchface${capsVariant}Output") {
val wfTask = project(":wear:watchface").tasks.named("assemble$capsVariant")
dependsOn(wfTask)
val buildDir = project(":wear:watchface").layout.buildDirectory.asFileTree.matching {
include("**/${variant.name}/**/*.apk")
exclude("**/*androidTest*")
val watchfaceApkConfig = when (variant.name) {
"release" -> configurations.getByName("watchfaceApkRelease")
"debug" -> configurations.getByName("watchfaceApkDebug")
else -> throw RuntimeException("Cannot find watchface apk configuration")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This when statement can be simplified by constructing the configuration name dynamically from the variant.name. This makes the code more concise and less prone to errors if new variants are added, as it removes the need to update this block manually.

    val watchfaceApkConfig = configurations.getByName("watchfaceApk$capsVariant")


val copyWatchfaceApkTask = tasks.register<Copy>("copyWatchface${capsVariant}ApkToAssets") {
from(watchfaceApkConfig) {
// the resolved directory contains apk and output-metadata.json
include("*.apk")
}
from(buildDir)
into(layout.buildDirectory.dir("intermediates/watchfaceAssets/${variant.name}"))

duplicatesStrategy = DuplicatesStrategy.EXCLUDE

eachFile {
path = "default_watchface.apk"
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
includeEmptyDirs = false
}

val tokenTask = tasks.register<ProcessFilesTask>("generateToken${capsVariant}Res") {
val tokenFile =
layout.buildDirectory.file("generated/wfTokenRes/${variant.name}/res/values/wf_token.xml")

inputFile.from(copyTaskProvider.map { it.outputs.files.singleFile })
inputFile.from(copyWatchfaceApkTask.map { it.outputs.files.singleFile })
outputFile.set(tokenFile)
cliToolClasspath.set(project.configurations["cliToolConfiguration"])
}
Expand Down Expand Up @@ -143,7 +174,8 @@ abstract class ProcessFilesTask : DefaultTask() {

@TaskAction
fun taskAction() {
val apkFile = inputFile.singleFile.resolve("default_watchface.apk")
val apkDirectory = inputFile.singleFile
val apkFile = apkDirectory.resolve("default_watchface.apk")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The inputFile for this task is a ConfigurableFileCollection which, in this case, contains a single APK file. Therefore, inputFile.singleFile returns the File object for the APK itself, not a directory. The variable name apkDirectory is misleading, and calling .resolve() on a file is incorrect. You should directly use the file from inputFile.

        val apkFile = inputFile.singleFile

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right @bingranl ? If so seems a reasonable suggestion

Copy link
Contributor Author

@bingranl bingranl Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inputFile is a FileCollection and the result from singleFile function doesn't have to be a file. In this case, apkDirectory is a directory containing the apk and output-metadata.json.

To improve readability, I manage to wire up the input and output a little differently and believe the new approach looks much better than before


val stdOut = ByteArrayOutputStream()
val stdErr = ByteArrayOutputStream()
Expand Down Expand Up @@ -189,4 +221,4 @@ abstract class ProcessFilesTask : DefaultTask() {
)
}
}
}
}