From 4ed3cd3c2c805da38cf22d1fc006d2da7c7e7223 Mon Sep 17 00:00:00 2001 From: garan Date: Fri, 14 Feb 2025 11:16:04 +0000 Subject: [PATCH] Adds support for version qualifiers --- .../watchface/dfx/memory/AndroidResource.kt | 39 ++++++--- .../dfx/memory/AndroidResourceLoader.kt | 15 +++- .../dfx/memory/ResourceMemoryEvaluator.java | 32 ++++++-- .../watchface/dfx/memory/WatchFaceData.kt | 7 +- .../watchface/dfx/memory/WatchFaceDocument.kt | 22 ++++++ .../dfx/memory/AndroidResourceTest.kt | 79 +++++++++++++++++++ .../dfx/memory/InputPackageTest.java | 22 ++---- .../memory/ResourceMemoryEvaluatorTest.java | 2 +- .../src/main/res/raw-v34/watchface.xml | 63 +++++++++++++++ 9 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceDocument.kt create mode 100644 play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/AndroidResourceTest.kt create mode 100644 play-validations/memory-footprint/test-samples/sample-wf/src/main/res/raw-v34/watchface.xml diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.kt b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.kt index f1d104c..9741aaf 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.kt +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.kt @@ -16,6 +16,8 @@ package com.google.wear.watchface.dfx.memory +import com.google.common.annotations.VisibleForTesting +import java.nio.file.InvalidPathException import java.nio.file.Path import java.nio.file.Paths import java.util.regex.Pattern @@ -27,12 +29,13 @@ class AndroidResource( // Resource name, for example "watchface" for res/raw/watchface.xml. val resourceName: String, // File extension of the resource, for example "xml" for res/raw/watchface.xml - private val extension: String, + @VisibleForTesting val extension: String, // Path in the package. This is the obfuscated path to the actual data, where obfuscation has // been used, for example "res/raw/watchface.xml" may point to something like "res/li.xml". val filePath: Path, // The resource data itself. - val data: ByteArray + val data: ByteArray, + val versionQualifier: Int = NO_VERSION_QUALIFIER, ) { // TODO: This should be improved to parse res/xml/watch_face_info.xml where present, so as not // to assume all XML files in the res/raw directory are watch face XML files. @@ -48,20 +51,34 @@ class AndroidResource( companion object { private val VALID_RESOURCE_PATH: Pattern = - Pattern.compile(".*res/([^-/]+).*/([^.]+)(\\.|)(.*|)$") - private const val VALID_RESOURCE_GROUPS: Int = 4 + Pattern.compile(".*res/([^-/]+)(|.*-v(\\d+)|-.*)/([^.]+)[.]?(.*|)$") + private const val VALID_RESOURCE_GROUPS: Int = 5 + const val NO_VERSION_QUALIFIER: Int = -1 @JvmStatic fun fromPath(filePath: Path, data: ByteArray): AndroidResource { val pathWithFwdSlashes = filePath.toString().replace('\\', '/') - val matcher = VALID_RESOURCE_PATH.matcher(pathWithFwdSlashes) - if (matcher.matches() && matcher.groupCount() == VALID_RESOURCE_GROUPS) { - val resType = matcher.group(1) - val resName = matcher.group(2) - val ext = matcher.group(4) - return AndroidResource(resType, resName, ext, filePath, data) + val m = VALID_RESOURCE_PATH.matcher(pathWithFwdSlashes) + + // Extracts both scenarios without a version resource qualifier, e.g. /res/raw and those + // with a version resource qualifier, e.g. /res/raw-v34 or /res/raw-round-v34. The + // version qualifier is always last in the list of qualifiers. + if (m.matches() && m.groupCount() == VALID_RESOURCE_GROUPS) { + val resType = m.group(1) + var qualifierVersion = NO_VERSION_QUALIFIER + if (m.group(2) != null && m.group(2).isNotEmpty() && + m.group(3) != null && m.group(3).isNotEmpty() + ) { + qualifierVersion = m.group(3).toInt() + } + val resName = m.group(4) + val ext = m.group(5) + return AndroidResource(resType, resName, ext, filePath, data, qualifierVersion) } - throw RuntimeException("Not a valid resource file: $pathWithFwdSlashes") + throw InvalidPathException( + filePath.toString(), + "Not a valid resource file" + ) } @JvmStatic diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.kt b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.kt index caf22d3..8d86c36 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.kt +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.kt @@ -20,6 +20,8 @@ import com.google.common.io.Files import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk +import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk +import com.google.wear.watchface.dfx.memory.AndroidResourceLoader.versionQualifier import java.io.IOException import java.io.InputStream import java.nio.file.Path @@ -127,16 +129,27 @@ object AndroidResourceLoader { .map { entry -> val path = stringPool.getString(entry.value().data()) val data = apkFile.getInputStream(ZipEntry(path)).readBytes() + AndroidResource( entry.parent().typeName, entry.key(), Files.getFileExtension(path), Paths.get(path), - data + data, + entry.versionQualifier ) } } @JvmStatic fun readAllBytes(steam: InputStream) = steam.readBytes() + + // If the entry in the table has a version qualifier, use that, otherwise use the special value + // to indicate that no version qualifier is present. + private val TypeChunk.Entry.versionQualifier: Int + get() = if (parent().configuration.sdkVersion() > 0) { + parent().configuration.sdkVersion() + } else { + AndroidResource.NO_VERSION_QUALIFIER + } } diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluator.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluator.java index 118c339..5d7ac89 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluator.java +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluator.java @@ -127,15 +127,19 @@ static List evaluateMemoryFootprint(EvaluationSettings evaluati + "does not match version in manifest (%s)%n", cliWffVersion, manifestWffVersion); } - validateFormat( - watchFaceData, cliWffVersion != null ? cliWffVersion : manifestWffVersion); + // If the CLI was used to set the version number then this version should be used + // for all WFF XML files, irrespective of the resource qualifier. + boolean useFixedVersionNumber = cliWffVersion != null; + String validationVersion = + cliWffVersion != null ? cliWffVersion : manifestWffVersion; + validateFormat(watchFaceData, validationVersion, useFixedVersionNumber); } return watchFaceData.getWatchFaceDocuments().stream() .map( watchFaceDocument -> evaluateWatchFaceForLayout( watchFaceData.getResourceDetailsMap(), - watchFaceDocument, + watchFaceDocument.getDocument(), evaluationSettings)) .collect(Collectors.toList()); } @@ -153,13 +157,29 @@ static MemoryFootprint evaluateWatchFaceForLayout( * * @param watchFaceData the watch face data containing the watchface xml documents. * @param watchFaceFormatVersion the watch face format version. + * @param useFixedVersionNumber whether to use only the specified version number or to override + * with the version number from any resource qualifier. This is useful when the version is + * specified on the command-line, and should therefore be fixed to that value irrespective. * @throws TestFailedException if the watch face does not comply to the format version. */ - private static void validateFormat(WatchFaceData watchFaceData, String watchFaceFormatVersion) { + private static void validateFormat( + WatchFaceData watchFaceData, + String watchFaceFormatVersion, + boolean useFixedVersionNumber) { WatchFaceXmlValidator xmlValidator = new WatchFaceXmlValidator(); - for (Document watchFaceDocument : watchFaceData.getWatchFaceDocuments()) { + for (WatchFaceDocument watchFaceDocument : watchFaceData.getWatchFaceDocuments()) { + String version; + // If the version of the watch face document was derived from a resource qualifier, e.g. + // being in a directory such as res/raw-v34/ indicating WFF v2, override the manifest + // specified version, unless useFixedVersionNumber is true. + if (watchFaceDocument.getWffVersion() == AndroidResource.NO_VERSION_QUALIFIER + || useFixedVersionNumber) { + version = watchFaceFormatVersion; + } else { + version = String.valueOf(watchFaceDocument.getWffVersion()); + } boolean documentHasValidSchema = - xmlValidator.validate(watchFaceDocument, watchFaceFormatVersion); + xmlValidator.validate(watchFaceDocument.getDocument(), version); if (!documentHasValidSchema) { throw new TestFailedException( "Watch Face has syntactic errors and cannot be parsed."); diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt index 575056c..eab3e11 100644 --- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt @@ -27,13 +27,13 @@ import kotlin.streams.asSequence internal class WatchFaceData private constructor() { /** Mutable backing field for [watchFaceDocuments]. */ - private val _watchFaceDocuments = mutableListOf() + private val _watchFaceDocuments = mutableListOf() /** * The parsed watchface xml documents. A watch face can have multiple layout files for different * screen shapes and resolutions. */ - val watchFaceDocuments: List = _watchFaceDocuments + val watchFaceDocuments: List = _watchFaceDocuments /** Mutable backing field for [resourceDetailsMap]. */ private val _resourceDetailsMap = mutableMapOf() @@ -100,7 +100,8 @@ internal class WatchFaceData private constructor() { if (resource.isWatchFaceXml()) { val document = parseXmlResource(resource.data) if (isWatchFaceDocument(document, evaluationSettings)) { - watchFaceData._watchFaceDocuments.add(document) + watchFaceData._watchFaceDocuments + .add(WatchFaceDocument(document, resource.versionQualifier)) continue } } diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceDocument.kt b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceDocument.kt new file mode 100644 index 0000000..9fc3189 --- /dev/null +++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceDocument.kt @@ -0,0 +1,22 @@ +package com.google.wear.watchface.dfx.memory + +import com.google.wear.watchface.dfx.memory.AndroidResource.Companion.NO_VERSION_QUALIFIER +import org.w3c.dom.Document + +data class WatchFaceDocument(val document: Document, private val versionQualifier: Int) { + companion object { + // If no WFF version has been attached to this document, namely, it came from an unqualified + // directory, e.g. /res/raw, not /res/raw-v34 etc. + const val NO_WFF_VERSION: Int = -1 + + // WFFv1 corresponds to API level 33, v2 to 34, etc. + const val WFF_VERSION_OFFSET: Int = 32 + } + + val wffVersion: Int + get() = if (versionQualifier == NO_VERSION_QUALIFIER) { + NO_WFF_VERSION + } else { + versionQualifier - WFF_VERSION_OFFSET + } +} \ No newline at end of file diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/AndroidResourceTest.kt b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/AndroidResourceTest.kt new file mode 100644 index 0000000..99bb870 --- /dev/null +++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/AndroidResourceTest.kt @@ -0,0 +1,79 @@ +package com.google.wear.watchface.dfx.memory + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.nio.file.InvalidPathException + +@RunWith(JUnit4::class) +class AndroidResourceTest { + @Test + fun fromPath_validResourceNoQualifiers() { + val path = "res/raw/watchface.xml" + val resource = AndroidResource.fromPath(path, byteArrayOf()) + + assertThat(resource.resourceName).isEqualTo("watchface") + assertThat(resource.isWatchFaceXml()).isEqualTo(true) + assertThat(resource.isRaw()).isEqualTo(true) + assertThat(resource.extension).isEqualTo("xml") + assertThat(resource.versionQualifier).isEqualTo(AndroidResource.NO_VERSION_QUALIFIER); + } + + @Test + fun fromPath_validResourceNoExtension() { + val path = "res/drawable/preview" + val resource = AndroidResource.fromPath(path, byteArrayOf()) + + assertThat(resource.resourceName).isEqualTo("preview") + assertThat(resource.isWatchFaceXml()).isEqualTo(false) + assertThat(resource.isDrawable()).isEqualTo(true) + assertThat(resource.extension).isEqualTo("") + } + + @Test + fun fromPath_invalidResourcePath() { + assertThrows(InvalidPathException::class.java) { + val path = "resxyz/drawable/preview.png" + AndroidResource.fromPath(path, byteArrayOf()) + } + } + + @Test + fun fromPath_validWatchfaceWithVersionQualifier() { + val path = "res/raw-v34/watchface.xml" + val resource = AndroidResource.fromPath(path, byteArrayOf()) + + assertThat(resource.resourceName).isEqualTo("watchface") + assertThat(resource.isWatchFaceXml()).isEqualTo(true) + assertThat(resource.isRaw()).isEqualTo(true) + assertThat(resource.extension).isEqualTo("xml") + assertThat(resource.versionQualifier).isEqualTo(34) + } + + @Test + fun fromPath_validResourceWithVersionQualifier() { + val path = "res/drawable-nodpi/image.png" + val resource = AndroidResource.fromPath(path, byteArrayOf()) + + assertThat(resource.resourceName).isEqualTo("image") + assertThat(resource.isWatchFaceXml()).isEqualTo(false) + assertThat(resource.isDrawable()).isEqualTo(true) + assertThat(resource.extension).isEqualTo("png") + assertThat(resource.versionQualifier).isEqualTo(AndroidResource.NO_VERSION_QUALIFIER); + } + + @Test + fun fromPath_validResourceWithMultipleQualifier() { + val path = "res/raw-round-v34/watchface.xml" + val resource = AndroidResource.fromPath(path, byteArrayOf()) + + assertThat(resource.resourceName).isEqualTo("watchface") + assertThat(resource.isWatchFaceXml()).isEqualTo(true) + assertThat(resource.isRaw()).isEqualTo(true) + assertThat(resource.extension).isEqualTo("xml") + assertThat(resource.versionQualifier).isEqualTo(34) + } +} \ No newline at end of file diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/InputPackageTest.java b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/InputPackageTest.java index a71d75e..2dca22a 100644 --- a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/InputPackageTest.java +++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/InputPackageTest.java @@ -3,7 +3,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.Streams; -import com.google.common.truth.Correspondence; import java.io.File; import java.nio.file.Path; import java.util.Arrays; @@ -34,32 +33,22 @@ public static Iterable parameters() { @Parameterized.Parameter(1) public String testAabDirectory; - private static final Correspondence VERIFY_PACKAGE_NAME_ONLY = - Correspondence.transforming( - packageFile -> packageFile.getFilePath().toString(), - "has the same file path as"); - @Test public void open_handlesFolder() { - List packageFiles; + List packageFileNames; AndroidManifest manifest; try (InputPackage inputPackage = InputPackage.open(testAabDirectory)) { - packageFiles = + packageFileNames = Streams.stream(inputPackage.getWatchFaceFiles().iterator()) + .map(x -> x.getFilePath().toString()) // remove this file, which is automatically created on MacOS - .filter( - x -> - !x.getFilePath() - .getFileName() - .toString() - .equals(".DS_Store")) + .filter(x -> !x.equals(".DS_Store")) .collect(Collectors.toList()); manifest = inputPackage.getManifest(); } - assertThat(packageFiles) - .comparingElementsUsing(VERIFY_PACKAGE_NAME_ONLY) + assertThat(packageFileNames) .containsExactly( "base/res/drawable-nodpi/bg.png", "base/res/drawable-nodpi/dial.png", @@ -71,6 +60,7 @@ public void open_handlesFolder() { "base/res/font/roboto_regular.ttf", "base/res/font/open_sans_regular.ttf", "base/res/raw/watchface.xml", + "base/res/raw-v34/watchface.xml", "base/res/values/strings.xml", "base/res/xml/watch_face_info.xml", "base/manifest/AndroidManifest.xml"); diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluatorTest.java b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluatorTest.java index 413bb21..26cec49 100644 --- a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluatorTest.java +++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/ResourceMemoryEvaluatorTest.java @@ -100,7 +100,7 @@ public static Collection data() { /* expectedActiveFootprintBytes= */ 4712628 + SYSTEM_DEFAULT_FONT_SIZE, /* expectedAmbientFootprintBytes= */ 2687628, - /* expectedLayouts= */ 1)) + /* expectedLayouts= */ 2)) .collect(Collectors.toList()); } diff --git a/play-validations/memory-footprint/test-samples/sample-wf/src/main/res/raw-v34/watchface.xml b/play-validations/memory-footprint/test-samples/sample-wf/src/main/res/raw-v34/watchface.xml new file mode 100644 index 0000000..7a399f2 --- /dev/null +++ b/play-validations/memory-footprint/test-samples/sample-wf/src/main/res/raw-v34/watchface.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + Sample Watch Face + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +