diff --git a/WatchFaceFormat/Complications/build.gradle.kts b/WatchFaceFormat/Complications/build.gradle.kts index c77a35442..68e631f21 100644 --- a/WatchFaceFormat/Complications/build.gradle.kts +++ b/WatchFaceFormat/Complications/build.gradle.kts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id("com.android.application") version "8.6.1" apply false -} \ No newline at end of file + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/WatchFaceFormat/Complications/gradle/libs.versions.toml b/WatchFaceFormat/Complications/gradle/libs.versions.toml new file mode 100644 index 000000000..4674692ae --- /dev/null +++ b/WatchFaceFormat/Complications/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + diff --git a/WatchFaceFormat/Complications/settings.gradle.kts b/WatchFaceFormat/Complications/settings.gradle.kts index c7dbf5a3e..eeee906eb 100644 --- a/WatchFaceFormat/Complications/settings.gradle.kts +++ b/WatchFaceFormat/Complications/settings.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ pluginManagement { + includeBuild("../validator-plugin") repositories { google() mavenCentral() @@ -27,6 +28,8 @@ dependencyResolutionManagement { mavenCentral() } } +// See: https://medium.com/@ttdevelopment/encountering-the-unable-to-make-progress-running-work-error-in-gradle-6bc363ac1eb8 +gradle.startParameter.excludedTaskNames.addAll(listOf(":validator-plugin:plugins:testClasses")) rootProject.name = "Complications" -include(":watchface") \ No newline at end of file +include(":watchface") diff --git a/WatchFaceFormat/Complications/watchface/build.gradle.kts b/WatchFaceFormat/Complications/watchface/build.gradle.kts index 1d5a2f692..ee2148dd1 100644 --- a/WatchFaceFormat/Complications/watchface/build.gradle.kts +++ b/WatchFaceFormat/Complications/watchface/build.gradle.kts @@ -14,7 +14,10 @@ * limitations under the License. */ plugins { - id("com.android.application") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // Use the locally-defined validator to demonstrate validation on-build. + id("com.google.wff.validatorplugin") } android { diff --git a/WatchFaceFormat/Flavors/build.gradle.kts b/WatchFaceFormat/Flavors/build.gradle.kts index c77a35442..68e631f21 100644 --- a/WatchFaceFormat/Flavors/build.gradle.kts +++ b/WatchFaceFormat/Flavors/build.gradle.kts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id("com.android.application") version "8.6.1" apply false -} \ No newline at end of file + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/WatchFaceFormat/Flavors/gradle/libs.versions.toml b/WatchFaceFormat/Flavors/gradle/libs.versions.toml new file mode 100644 index 000000000..4674692ae --- /dev/null +++ b/WatchFaceFormat/Flavors/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + diff --git a/WatchFaceFormat/Flavors/settings.gradle.kts b/WatchFaceFormat/Flavors/settings.gradle.kts index e9f65066f..34e36dacb 100644 --- a/WatchFaceFormat/Flavors/settings.gradle.kts +++ b/WatchFaceFormat/Flavors/settings.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ pluginManagement { + includeBuild("../validator-plugin") repositories { google() mavenCentral() @@ -27,6 +28,8 @@ dependencyResolutionManagement { mavenCentral() } } +// See: https://medium.com/@ttdevelopment/encountering-the-unable-to-make-progress-running-work-error-in-gradle-6bc363ac1eb8 +gradle.startParameter.excludedTaskNames.addAll(listOf(":validator-plugin:plugins:testClasses")) rootProject.name = "Flavors" -include(":watchface") \ No newline at end of file +include(":watchface") diff --git a/WatchFaceFormat/Flavors/watchface/build.gradle.kts b/WatchFaceFormat/Flavors/watchface/build.gradle.kts index 4fc85ad8f..cc78e0af6 100644 --- a/WatchFaceFormat/Flavors/watchface/build.gradle.kts +++ b/WatchFaceFormat/Flavors/watchface/build.gradle.kts @@ -14,7 +14,10 @@ * limitations under the License. */ plugins { - id("com.android.application") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // Use the locally-defined validator to demonstrate validation on-build. + id("com.google.wff.validatorplugin") } android { diff --git a/WatchFaceFormat/SimpleAnalog/build.gradle.kts b/WatchFaceFormat/SimpleAnalog/build.gradle.kts index c77a35442..68e631f21 100644 --- a/WatchFaceFormat/SimpleAnalog/build.gradle.kts +++ b/WatchFaceFormat/SimpleAnalog/build.gradle.kts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id("com.android.application") version "8.6.1" apply false -} \ No newline at end of file + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/WatchFaceFormat/SimpleAnalog/gradle/libs.versions.toml b/WatchFaceFormat/SimpleAnalog/gradle/libs.versions.toml new file mode 100644 index 000000000..4674692ae --- /dev/null +++ b/WatchFaceFormat/SimpleAnalog/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + diff --git a/WatchFaceFormat/SimpleAnalog/settings.gradle.kts b/WatchFaceFormat/SimpleAnalog/settings.gradle.kts index 47babca81..8cd745286 100644 --- a/WatchFaceFormat/SimpleAnalog/settings.gradle.kts +++ b/WatchFaceFormat/SimpleAnalog/settings.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ pluginManagement { + includeBuild("../validator-plugin") repositories { google() mavenCentral() @@ -27,6 +28,8 @@ dependencyResolutionManagement { mavenCentral() } } +// See: https://medium.com/@ttdevelopment/encountering-the-unable-to-make-progress-running-work-error-in-gradle-6bc363ac1eb8 +gradle.startParameter.excludedTaskNames.addAll(listOf(":validator-plugin:plugins:testClasses")) rootProject.name = "SimpleAnalog" -include(":watchface") \ No newline at end of file +include(":watchface") diff --git a/WatchFaceFormat/SimpleAnalog/watchface/build.gradle.kts b/WatchFaceFormat/SimpleAnalog/watchface/build.gradle.kts index 3d4df673b..2306bac48 100644 --- a/WatchFaceFormat/SimpleAnalog/watchface/build.gradle.kts +++ b/WatchFaceFormat/SimpleAnalog/watchface/build.gradle.kts @@ -14,7 +14,10 @@ * limitations under the License. */ plugins { - id("com.android.application") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // Use the locally-defined validator to demonstrate validation on-build. + id("com.google.wff.validatorplugin") } android { diff --git a/WatchFaceFormat/SimpleDigital/build.gradle.kts b/WatchFaceFormat/SimpleDigital/build.gradle.kts index c77a35442..68e631f21 100644 --- a/WatchFaceFormat/SimpleDigital/build.gradle.kts +++ b/WatchFaceFormat/SimpleDigital/build.gradle.kts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id("com.android.application") version "8.6.1" apply false -} \ No newline at end of file + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/WatchFaceFormat/SimpleDigital/gradle/libs.versions.toml b/WatchFaceFormat/SimpleDigital/gradle/libs.versions.toml new file mode 100644 index 000000000..4674692ae --- /dev/null +++ b/WatchFaceFormat/SimpleDigital/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + diff --git a/WatchFaceFormat/SimpleDigital/settings.gradle.kts b/WatchFaceFormat/SimpleDigital/settings.gradle.kts index cf81b234d..9c48b5f66 100644 --- a/WatchFaceFormat/SimpleDigital/settings.gradle.kts +++ b/WatchFaceFormat/SimpleDigital/settings.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ pluginManagement { + includeBuild("../validator-plugin") repositories { google() mavenCentral() @@ -27,6 +28,8 @@ dependencyResolutionManagement { mavenCentral() } } +// See: https://medium.com/@ttdevelopment/encountering-the-unable-to-make-progress-running-work-error-in-gradle-6bc363ac1eb8 +gradle.startParameter.excludedTaskNames.addAll(listOf(":validator-plugin:plugins:testClasses")) rootProject.name = "SimpleDigital" -include(":watchface") \ No newline at end of file +include(":watchface") diff --git a/WatchFaceFormat/SimpleDigital/watchface/build.gradle.kts b/WatchFaceFormat/SimpleDigital/watchface/build.gradle.kts index 17af86e16..bc3667a47 100644 --- a/WatchFaceFormat/SimpleDigital/watchface/build.gradle.kts +++ b/WatchFaceFormat/SimpleDigital/watchface/build.gradle.kts @@ -14,7 +14,10 @@ * limitations under the License. */ plugins { - id("com.android.application") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // Use the locally-defined validator to demonstrate validation on-build. + id("com.google.wff.validatorplugin") } android { diff --git a/WatchFaceFormat/Weather/build.gradle.kts b/WatchFaceFormat/Weather/build.gradle.kts index c77a35442..68e631f21 100644 --- a/WatchFaceFormat/Weather/build.gradle.kts +++ b/WatchFaceFormat/Weather/build.gradle.kts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - plugins { - id("com.android.application") version "8.6.1" apply false -} \ No newline at end of file + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/WatchFaceFormat/Weather/gradle/libs.versions.toml b/WatchFaceFormat/Weather/gradle/libs.versions.toml new file mode 100644 index 000000000..4674692ae --- /dev/null +++ b/WatchFaceFormat/Weather/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + + diff --git a/WatchFaceFormat/Weather/settings.gradle.kts b/WatchFaceFormat/Weather/settings.gradle.kts index aee002fc6..fd99d5567 100644 --- a/WatchFaceFormat/Weather/settings.gradle.kts +++ b/WatchFaceFormat/Weather/settings.gradle.kts @@ -14,6 +14,7 @@ * limitations under the License. */ pluginManagement { + includeBuild("../validator-plugin") repositories { google() mavenCentral() @@ -27,6 +28,8 @@ dependencyResolutionManagement { mavenCentral() } } +// See: https://medium.com/@ttdevelopment/encountering-the-unable-to-make-progress-running-work-error-in-gradle-6bc363ac1eb8 +gradle.startParameter.excludedTaskNames.addAll(listOf(":validator-plugin:plugins:testClasses")) rootProject.name = "Weather" -include(":watchface") \ No newline at end of file +include(":watchface") diff --git a/WatchFaceFormat/Weather/watchface/build.gradle.kts b/WatchFaceFormat/Weather/watchface/build.gradle.kts index 2adc67088..b3a12c6c3 100644 --- a/WatchFaceFormat/Weather/watchface/build.gradle.kts +++ b/WatchFaceFormat/Weather/watchface/build.gradle.kts @@ -14,7 +14,10 @@ * limitations under the License. */ plugins { - id("com.android.application") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // Use the locally-defined validator to demonstrate validation on-build. + id("com.google.wff.validatorplugin") } android { diff --git a/WatchFaceFormat/validator-plugin/gradle.properties b/WatchFaceFormat/validator-plugin/gradle.properties new file mode 100644 index 000000000..3dcf88f02 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/gradle.properties @@ -0,0 +1,2 @@ +# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534 +org.gradle.parallel=true diff --git a/WatchFaceFormat/validator-plugin/gradle/libs.versions.toml b/WatchFaceFormat/validator-plugin/gradle/libs.versions.toml new file mode 100644 index 000000000..507eb1288 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = "8.7.0" +kotlin = "1.9.25" + +[libraries] +android-gradlePlugin-api = { group = "com.android.tools.build", name = "gradle-api", version.ref = "androidGradlePlugin" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/WatchFaceFormat/validator-plugin/gradle/wrapper/gradle-wrapper.jar b/WatchFaceFormat/validator-plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e69de29bb diff --git a/WatchFaceFormat/validator-plugin/gradle/wrapper/gradle-wrapper.properties b/WatchFaceFormat/validator-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..09523c0e5 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/WatchFaceFormat/validator-plugin/gradlew b/WatchFaceFormat/validator-plugin/gradlew new file mode 100755 index 000000000..e69de29bb diff --git a/WatchFaceFormat/validator-plugin/gradlew.bat b/WatchFaceFormat/validator-plugin/gradlew.bat new file mode 100644 index 000000000..e69de29bb diff --git a/WatchFaceFormat/validator-plugin/plugins/build.gradle.kts b/WatchFaceFormat/validator-plugin/plugins/build.gradle.kts new file mode 100644 index 000000000..05098e190 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + `java-gradle-plugin` + alias(libs.plugins.kotlin.jvm) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation("com.android.tools.build:gradle-api:7.1.0-alpha13") + + implementation("io.ktor:ktor-client-core:3.0.0") + runtimeOnly("io.ktor:ktor-client-okhttp:3.0.0") + + compileOnly(libs.android.gradlePlugin.api) + implementation(gradleKotlinDsl()) +} + +gradlePlugin { + plugins { + create("wffValidatorPlugin") { + id = "com.google.wff.validatorplugin" + implementationClass = "WffValidatorPlugin" + } + } +} diff --git a/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/AdbInstallTask.kt b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/AdbInstallTask.kt new file mode 100644 index 000000000..4e2efcb7c --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/AdbInstallTask.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.android.build.api.variant.BuiltArtifactsLoader +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import java.io.File + +const val SET_WATCH_FACE_CMD = + "shell am broadcast -a com.google.android.wearable.app.DEBUG_SURFACE --es operation set-watchface --es watchFaceId %s" + +/** + * Installs and sets watch face on an attached device via ADB. + */ +open class AdbInstallTask : DefaultTask() { + @set:Option(option = "device", description = "The ADB device to install on") + @get:Input + var device: String = "" + + @get:Input + lateinit var apkLocation: Provider<Directory> + + @get:Input + lateinit var artifactLoader: BuiltArtifactsLoader + + // As this task has no outputs defined, it will always be executed, which is desirable as the + // APK should be installed even if the APK itself hasn't changed. (It may have been removed from + // the device). + + @TaskAction + fun install() { + val artifacts = + artifactLoader.load(apkLocation.get()) ?: throw GradleException("Cannot load APKs") + if (artifacts.elements.size != 1) + throw GradleException("Expected only one APK!") + val apkPath = File(artifacts.elements.single().outputFile).toPath() + + val deviceArg = if (device.isEmpty()) "" else "-s $device " + val installWatchFaceCmd = deviceArg + "install $apkPath" + val setWatchFaceCmd = deviceArg + SET_WATCH_FACE_CMD.format(artifacts.applicationId) + + project.exec { + val argsList = installWatchFaceCmd.split(" ").toList() + it.commandLine("adb") + it.args(argsList) + } + + project.exec { + val argsList = setWatchFaceCmd.split(" ").toList() + it.commandLine("adb") + it.args(argsList) + } + } +} diff --git a/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ManifestTools.kt b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ManifestTools.kt new file mode 100644 index 000000000..48fdee17e --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ManifestTools.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import groovy.xml.Namespace +import groovy.xml.XmlSlurper +import groovy.xml.slurpersupport.GPathResult +import groovy.xml.slurpersupport.Node +import org.gradle.api.GradleException + +const val WFF_PROP_NAME = "com.google.wear.watchface.format.version" + +internal fun String.withNS(ns: Namespace) = "{${ns.uri}}$this" + +/** + * Obtains the Watch Face Format version from the AndroidManifest.xml, or throws an error if a valid + * value cannot be found. + */ +internal fun getWffVersion(manifestPath: String): Int { + val manifestXml = + XmlSlurper(false, true).parse(manifestPath) + val ns = Namespace("http://schemas.android.com/apk/res/android", "android") + val applicationNode = manifestXml.getProperty("application") as GPathResult + val versionProp = applicationNode.childNodes().asSequence().firstOrNull { + val node = it as? Node + node?.name() == "property" && WFF_PROP_NAME == node.attributes()?.get("name".withNS(ns)) + } as Node? + if (versionProp == null) { + throw GradleException("AndroidManifest.xml does not contain, or has invalid WFF version property") + } + val valueAttr = versionProp.attributes()?.get("value".withNS(ns)) + ?: throw GradleException("WFF version property does not have a value attribute") + + return valueAttr.toString().toIntOrNull() + ?: throw GradleException("WFF version is not a valid integer") +} diff --git a/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidateWffFilesTask.kt b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidateWffFilesTask.kt new file mode 100644 index 000000000..171fc5115 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidateWffFilesTask.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.ChangeType +import org.gradle.work.Incremental +import org.gradle.work.InputChanges + +/** + * Runs the validator against WFF XML files. + */ +@CacheableTask +abstract class ValidateWffFilesTask : DefaultTask() { + init { + this.outputs.upToDateWhen { true } + } + + @get:InputFiles + @get:Incremental + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val wffFiles: ConfigurableFileCollection + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val validatorJarPath: RegularFileProperty + + @get:Input + abstract val wffVersion: Property<Int> + + @TaskAction + fun validate(inputs: InputChanges) { + val changedFiles = inputs.getFileChanges(wffFiles) + changedFiles.forEach { change -> + if (change.changeType != ChangeType.REMOVED) { + project.javaexec { + it.classpath = project.files(validatorJarPath) + // Stop-on-fail ensures that the Gradle Task throws an exception when a WFF file fails + // to validate. + it.args(wffVersion.get().toString(), "--stop-on-fail", change.file.absolutePath) + } + } + } + } +} + diff --git a/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidatorDownloadTask.kt b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidatorDownloadTask.kt new file mode 100644 index 000000000..e9a97832d --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/ValidatorDownloadTask.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.util.cio.writeChannel +import io.ktor.utils.io.copyAndClose +import kotlinx.coroutines.runBlocking +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.nio.file.Path + +/** + * Downloads the WFF validator for use in the build process. + */ +@CacheableTask +abstract class ValidatorDownloadTask : DefaultTask() { + @get:OutputFile + abstract val validatorJarPath: RegularFileProperty + + @get:Input + abstract val validatorUrl: Property<String> + + @TaskAction + fun install() { + downloadFileToPath(validatorJarPath.get().asFile.toPath(), validatorUrl.get()) + } + + private fun downloadFileToPath(filePath: Path, url: String) { + val client = HttpClient { expectSuccess = true } + val file = filePath.toFile() + + // The validator generally won't exist already -- but there is the potential for the input + // URL to change, in which case the existing validator should be removed. + if (file.exists()) { + file.delete() + } + + runBlocking { + client.get(url).bodyAsChannel().copyAndClose(filePath.toFile().writeChannel()) + } + } +} diff --git a/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/WffValidatorPlugin.kt b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/WffValidatorPlugin.kt new file mode 100644 index 000000000..44f70f556 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/plugins/src/main/kotlin/WffValidatorPlugin.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.BuiltArtifactsLoader +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.register + +const val ASSEMBLE_DEBUG_TASK = "assembleDebug" +const val BUNDLE_DEBUG_TASK = "bundleDebug" +const val VALIDATE_TASK = "validateWff" +const val DOWNLOAD_VALIDATOR_TASK = "downloadWffValidator" +const val INSTALL_TASK = "validateWffAndInstall" + +// TODO move from here +private const val VALIDATOR_URL = + "https://github.com/google/watchface/releases/download/release/dwf-format-2-validator-1.0.jar" +private const val VALIDATOR_PATH = "validator/validator.jar" + +class WffValidatorPlugin : Plugin<Project> { + private lateinit var manifestPath: String + + override fun apply(project: Project) { + val downloadTask = project.tasks.register<ValidatorDownloadTask>(DOWNLOAD_VALIDATOR_TASK) { + val validatorPath = project.layout.buildDirectory.file(VALIDATOR_PATH) + this.validatorUrl.set(VALIDATOR_URL) + this.validatorJarPath.set(validatorPath) + } + + project.tasks.register<ValidateWffFilesTask>(VALIDATE_TASK) { + val wffFileCollection = getWffFileCollection(project) + if (wffFileCollection.isEmpty) { + throw GradleException("No WFF XML files found in project!") + } + validatorJarPath.set(downloadTask.get().validatorJarPath) + wffFiles.setFrom(wffFileCollection) + wffVersion.set(getWffVersion(manifestPath)) + } + + val androidComponents = + project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java) + + lateinit var apkDirectoryProvider: Provider<Directory> + lateinit var loader: BuiltArtifactsLoader + + androidComponents.onVariants(androidComponents.selector().withName("debug")) { variant -> + manifestPath = + variant.sources.manifests.all.get().firstOrNull { it.asFile.exists() }?.toString() + ?: throw GradleException("No AndroidManifest.xml found!") + + apkDirectoryProvider = variant.artifacts.get(SingleArtifact.APK) + loader = variant.artifacts.getBuiltArtifactsLoader() + } + + project.afterEvaluate { proj -> + // Ensure that validation is run as part of the debug build for APKs and for bundles. + proj.tasks[ASSEMBLE_DEBUG_TASK].dependsOn(VALIDATE_TASK) + proj.tasks[BUNDLE_DEBUG_TASK].dependsOn(VALIDATE_TASK) + + // Register additional task that allows for installing and setting of watch face. + proj.tasks.register<AdbInstallTask>(INSTALL_TASK) { + apkLocation = apkDirectoryProvider + artifactLoader = loader + dependsOn(ASSEMBLE_DEBUG_TASK) + } + } + } + + private fun getWffFileCollection(project: Project): FileCollection { + return project.layout.files("src/main/res/").asFileTree + .filter { it.isFile } + .filter { it.name.endsWith(".xml") } + .filter { it.parentFile.name.startsWith("raw") } + } +} + + diff --git a/WatchFaceFormat/validator-plugin/settings.gradle.kts b/WatchFaceFormat/validator-plugin/settings.gradle.kts new file mode 100644 index 000000000..9191f5dc0 --- /dev/null +++ b/WatchFaceFormat/validator-plugin/settings.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + + + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +include(":plugins")