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")