Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,19 @@ static List<MemoryFootprint> 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());
}
Expand All @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ import kotlin.streams.asSequence
internal class WatchFaceData private constructor() {

/** Mutable backing field for [watchFaceDocuments]. */
private val _watchFaceDocuments = mutableListOf<Document>()
private val _watchFaceDocuments = mutableListOf<WatchFaceDocument>()

/**
* The parsed watchface xml documents. A watch face can have multiple layout files for different
* screen shapes and resolutions.
*/
val watchFaceDocuments: List<Document> = _watchFaceDocuments
val watchFaceDocuments: List<WatchFaceDocument> = _watchFaceDocuments

/** Mutable backing field for [resourceDetailsMap]. */
private val _resourceDetailsMap = mutableMapOf<String, DrawableResourceDetails>()
Expand Down Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,32 +33,22 @@ public static Iterable<?> parameters() {
@Parameterized.Parameter(1)
public String testAabDirectory;

private static final Correspondence<AndroidResource, String> VERIFY_PACKAGE_NAME_ONLY =
Correspondence.transforming(
packageFile -> packageFile.getFilePath().toString(),
"has the same file path as");

@Test
public void open_handlesFolder() {
List<AndroidResource> packageFiles;
List<String> 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",
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static Collection<Object> data() {
/* expectedActiveFootprintBytes= */ 4712628
+ SYSTEM_DEFAULT_FONT_SIZE,
/* expectedAmbientFootprintBytes= */ 2687628,
/* expectedLayouts= */ 1))
/* expectedLayouts= */ 2))
.collect(Collectors.toList());
}

Expand Down
Loading
Loading