diff --git a/play-validations/gradle.properties b/play-validations/gradle.properties
index 7f9e379..6d2c02a 100644
--- a/play-validations/gradle.properties
+++ b/play-validations/gradle.properties
@@ -36,5 +36,4 @@ android.builder.sdkDownload=false
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
-android.suppressUnsupportedCompileSdk=34
-android.enableResourceOptimizations=false
\ No newline at end of file
+android.suppressUnsupportedCompileSdk=34
\ No newline at end of file
diff --git a/play-validations/memory-footprint/build.gradle b/play-validations/memory-footprint/build.gradle
index bad75f6..efa4c7c 100644
--- a/play-validations/memory-footprint/build.gradle
+++ b/play-validations/memory-footprint/build.gradle
@@ -24,6 +24,7 @@ dependencies {
implementation 'com.google.guava:guava:32.0.1-jre'
implementation 'com.google.mug:mug-guava:6.6'
implementation 'commons-cli:commons-cli:1.5.0'
+ implementation 'com.android.tools.apkparser:binary-resources:31.4.0'
implementation project(':validator')
testImplementation 'junit:junit:4.13.2'
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.java
new file mode 100644
index 0000000..f3c34ad
--- /dev/null
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.java
@@ -0,0 +1,101 @@
+package com.google.wear.watchface.dfx.memory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a resource, from an AAB or APK.
+ */
+public class AndroidResource {
+ private static final Pattern VALID_RESOURCE_PATH = Pattern.compile(".*res/([^-/]+).*/([^.]+)(\\.|)(.*|)$");
+ private static final int VALID_RESOURCE_GROUPS = 4;
+
+ // Resource type, for example "raw", "asset", "drawable" etc.
+ private final String resourceType;
+
+ // Resource name, for example "watchface" for res/raw/watchface.xml.
+ private final String resourceName;
+
+ // File extension of the resource, for example "xml" for res/raw/watchface.xml
+ private final String extension;
+
+ // 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".
+ private final Path filePath;
+
+ // The resource data itself.
+ private final byte[] data;
+
+ public AndroidResource(
+ String resourceType,
+ String resourceName,
+ String extension,
+ Path filePath,
+ byte[] data
+ ) {
+ this.resourceType = resourceType;
+ this.resourceName = resourceName;
+ this.extension = extension;
+ this.filePath = filePath;
+ this.data = data;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ public Path getFilePath() {
+ return filePath;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ // 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.
+ public Boolean isWatchFaceXml() {
+ return "xml".equals(extension) && "raw".equals(resourceType);
+ }
+
+ public Boolean isDrawable() { return "drawable".equals(resourceType); }
+
+ public Boolean isFont() { return "font".equals(resourceType); }
+
+ public Boolean isAsset() { return "asset".equals(resourceType); }
+
+ public Boolean isRaw() { return "raw".equals(resourceType); }
+
+ static AndroidResource fromPath(Path filePath, byte[] data) {
+ String pathWithFwdSlashes = filePath.toString().replace('\\', '/');
+ Matcher m = VALID_RESOURCE_PATH.matcher(pathWithFwdSlashes);
+ if (m.matches() && m.groupCount() == VALID_RESOURCE_GROUPS) {
+ String resType = m.group(1);
+ String resName = m.group(2);
+ String ext = m.group(4);
+ return new AndroidResource(
+ resType,
+ resName,
+ ext,
+ filePath,
+ data
+ );
+ }
+ throw new RuntimeException("Not a valid resource file: " + m.matches());
+ }
+
+ static AndroidResource fromPath(String filePath, byte[] data) {
+ return fromPath(Paths.get(filePath), data);
+ }
+
+ static Boolean isValidResourcePath(Path filePath) {
+ Matcher m = VALID_RESOURCE_PATH.matcher(filePath.toString());
+ return m.matches() && m.groupCount() == VALID_RESOURCE_GROUPS;
+ }
+
+ static Boolean isValidResourcePath(String filePath) {
+ return isValidResourcePath(Paths.get(filePath));
+ }
+}
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.java
new file mode 100644
index 0000000..3798fed
--- /dev/null
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.java
@@ -0,0 +1,237 @@
+package com.google.wear.watchface.dfx.memory;
+
+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.Chunk;
+import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk;
+import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk;
+import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+/**
+ * Represents all the resources of interest in the Android package.
+ *
+ * Where obfuscation has been applied, the mapping is derived, for the creation of the
+ * AndroidResource objects. For example, for a logical resource res/raw/watchface.xml, an APK
+ * may in fact store this as res/aB.xml. The resources.arsc file contains this mapping, and the
+ * AndroidResourceLoader provides a stream of resources with their logical types, names and data.
+ *
+ * Note that more than one AndroidResource object can exist for the given dimensions. For example,
+ * if there is a drawable and a drawable-fr folder, then there may be multiple AndroidResource
+ * entries for drawables with the same type, name and extension. The configuration detail, e.g.
+ * "fr" or "default", is not currently exposed in the AndroidResource objects as it isn't used.
+ */
+public class AndroidResourceLoader {
+ // Only certain resource types are of interest to the evaluator, notably, not string resources.
+ private static final Set RESOURCE_TYPES = Set.of("raw", "xml", "drawable", "font", "asset");
+ private static final String RESOURCES_FILE_NAME = "resources.arsc";
+
+ private AndroidResourceLoader() { }
+
+ /**
+ * Creates a resource stream from a path to an AAB structure on the file system.
+ *
+ * @param aabPath The path to the root of the AAB directory.
+ * @return A stream of Android resource objects.
+ * @throws IOException when the resources file cannot be found, or other IO errors occur.
+ */
+ static Stream streamFromAabDirectory(Path aabPath) throws IOException {
+ Stream childrenFilesStream = java.nio.file.Files.walk(aabPath);
+ int relativePathOffset = aabPath.toString().length() + 1;
+
+ return childrenFilesStream
+ .map(
+ filePath -> {
+ AndroidResource resource = null;
+ if (AndroidResource.isValidResourcePath(filePath)) {
+ try {
+ resource = AndroidResource.fromPath(
+ Paths.get(filePath.toString().substring(relativePathOffset)),
+ java.nio.file.Files.readAllBytes(filePath)
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (filePath.endsWith("manifest/AndroidManifest.xml")) {
+ try {
+ resource = new AndroidResource(
+ "xml",
+ "AndroidManifest",
+ "xml",
+ Paths.get(filePath.toString().substring(relativePathOffset)),
+ java.nio.file.Files.readAllBytes(filePath)
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return resource;
+ }
+ ).filter(Objects::nonNull);
+ }
+
+ /**
+ * Creates a stream of resource objects from the AAB file.
+ *
+ * @param aabZipFile The zip file object representing the AAB.
+ * @return A stream of resource objects.
+ */
+ static Stream streamFromAabFile(ZipFile aabZipFile) {
+ return aabZipFile.stream()
+ .map(
+ zipEntry -> {
+ Path zipEntryPath = Paths.get(zipEntry.getName());
+ AndroidResource resource = null;
+ if (AndroidResource.isValidResourcePath(zipEntryPath)) {
+ try {
+ resource = AndroidResource.fromPath(
+ zipEntryPath,
+ aabZipFile.getInputStream(zipEntry).readAllBytes()
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (zipEntry.getName().endsWith("manifest/AndroidManifest.xml")) {
+ try {
+ resource = new AndroidResource(
+ "xml",
+ "AndroidManifest",
+ "xml",
+ Paths.get(zipEntry.getName()),
+ aabZipFile.getInputStream(zipEntry).readAllBytes()
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return resource;
+ }
+ ).filter(Objects::nonNull);
+ }
+
+ /**
+ * Creates a resource stream from a base split entry within an archive
+ *
+ * @param baseSplitZipStream The zip entry for the base split.
+ * @return A stream of resource objects.
+ * @throws IOException when the resources file cannot be found, or other IO errors occur.
+ */
+ static Stream streamFromMokkaZip(ZipInputStream baseSplitZipStream) throws IOException {
+
+ Iterator iterator =
+ new Iterator() {
+ private ZipEntry zipEntry;
+
+ @Override
+ public boolean hasNext() {
+ try {
+ zipEntry = baseSplitZipStream.getNextEntry();
+ // Advance over entries in the zip that aren't relevant.
+ while (zipEntry != null && !AndroidResource.isValidResourcePath(zipEntry.getName())) {
+ zipEntry = baseSplitZipStream.getNextEntry();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return zipEntry != null;
+ }
+
+ @Override
+ public AndroidResource next() {
+ System.out.println(zipEntry);
+ byte[] entryData;
+ try {
+ entryData = readAllBytes(baseSplitZipStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return AndroidResource.fromPath(zipEntry.getName(), entryData);
+ }
+ };
+
+ return StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
+ false);
+ }
+
+ /**
+ * Creates a resource stream from an APK zip file.
+ *
+ * APK files can have their resources obfuscated, so it is necessary to extract the mapping
+ * between the original path and the path in the obfuscated zip.
+ *
+ * @param zipFile The APK zip file
+ * @return A stream of resource objects.
+ * @throws IOException when errors loading resources occur.
+ */
+ static Stream streamFromApkFile(ZipFile zipFile) throws IOException {
+ ZipEntry arscEntry = new ZipEntry(RESOURCES_FILE_NAME);
+ InputStream is = zipFile.getInputStream(arscEntry);
+
+ BinaryResourceFile resources = BinaryResourceFile.fromInputStream(is);
+ List chunks = resources.getChunks();
+ if (chunks.isEmpty()) {
+ throw new IOException("no chunks");
+ }
+ if (!(chunks.get(0) instanceof ResourceTableChunk)) {
+ throw new IOException("no res table chunk");
+ }
+ ResourceTableChunk table = (ResourceTableChunk) chunks.get(0);
+ StringPoolChunk stringPool = table.getStringPool();
+
+ List typeChunks = table.getPackages()
+ .stream()
+ .flatMap(p -> p.getTypeChunks().stream())
+ .toList();
+
+ return typeChunks.stream()
+ .flatMap(c -> c.getEntries().values().stream())
+ .filter(t -> RESOURCE_TYPES.contains(t.typeName()))
+ .filter(t -> t.value().type() == BinaryResourceValue.Type.STRING)
+ .map(entry -> {
+ Path path = Path.of(stringPool.getString(entry.value().data()));
+ byte[] data = null;
+ try {
+ data = zipFile.getInputStream(new ZipEntry(path.toString())).readAllBytes();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return new AndroidResource(
+ entry.parent().getTypeName(),
+ entry.key(),
+ Files.getFileExtension(path.toString()),
+ path,
+ data
+ );
+ });
+ }
+
+ /** Read all bytes from an input stream to a new byte array. */
+ static byte[] readAllBytes(InputStream inputStream) throws IOException {
+ int len;
+ byte[] buffer = new byte[1024];
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ while ((len = inputStream.read(buffer)) > 0) {
+ bos.write(buffer, 0, len);
+ }
+ return bos.toByteArray();
+ }
+}
\ No newline at end of file
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java
index ed0c7b2..4a27827 100644
--- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetails.java
@@ -16,8 +16,6 @@
package com.google.wear.watchface.dfx.memory;
-import static com.google.wear.watchface.dfx.memory.InputPackage.pathMatchesGlob;
-
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -38,9 +36,6 @@
/** Details about a drawable resource that are relevant for the memory footprint calculation. */
class DrawableResourceDetails {
- private static final int CHANNEL_MASK_R = 0xff;
- private static final int CHANNEL_MASK_G = 0xff00;
- private static final int CHANNEL_MASK_B = 0xff0000;
private static final int CHANNEL_MASK_A = 0xff000000;
/**
@@ -128,26 +123,24 @@ public java.lang.String toString() {
* number_of_frames * width * height * 4 bytes, or how much memory does storing that resource
* take in its uncompressed format.
*
- * @param packageFile the file from a watch face package.
+ * @param resource the resource from a watch face package.
* @return the memory footprint of that asset file or {@code Optional.empty()} if the file is
* not a drawable asset.
* @throws java.lang.IllegalArgumentException when the image cannot be processed.
*/
- static Optional fromPackageFile(InputPackage.PackageFile packageFile) {
+ static Optional fromPackageResource(AndroidResource resource) {
// For fonts we assume the raw size of the resource is the MCU footprint.
- if (pathMatchesGlob(packageFile.getFilePath(), "**/font/**")) {
+ if (resource.isFont()) {
return Optional.of(
new Builder()
- .setName(getResourceName(packageFile))
+ .setName(resource.getResourceName())
.setNumberOfImages(1)
- .setBiggestFrameFootprintBytes(packageFile.getData().length)
+ .setBiggestFrameFootprintBytes(resource.getData().length)
.build());
}
boolean isPossibleImage =
- pathMatchesGlob(packageFile.getFilePath(), "**/drawable*/*")
- || pathMatchesGlob(packageFile.getFilePath(), "**/assets/**")
- || pathMatchesGlob(packageFile.getFilePath(), "**/raw/*");
+ resource.isAsset() || resource.isDrawable() || resource.isRaw();
if (!isPossibleImage) {
return Optional.empty();
@@ -156,14 +149,14 @@ static Optional fromPackageFile(InputPackage.PackageFil
String sha1;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
- sha1 = byteArray2Hex(md.digest(packageFile.getData()));
+ sha1 = byteArray2Hex(md.digest(resource.getData()));
} catch (Exception e) {
throw new IllegalArgumentException(
- String.format("Error while processing image %s", packageFile.getFilePath()), e);
+ String.format("Error while processing image %s", resource.getFilePath()), e);
}
try (ImageInputStream imageInputStream =
- ImageIO.createImageInputStream(new ByteArrayInputStream(packageFile.getData()))) {
+ ImageIO.createImageInputStream(new ByteArrayInputStream(resource.getData()))) {
Iterator imageReaders = ImageIO.getImageReaders(imageInputStream);
if (!imageReaders.hasNext()) {
return Optional.empty();
@@ -201,7 +194,7 @@ static Optional fromPackageFile(InputPackage.PackageFil
boolean canBeQuantized = (maxQuantizationError < MAX_ACCEPTIABLE_QUANTIZATION_ERROR);
return Optional.of(
new Builder()
- .setName(getResourceName(packageFile))
+ .setName(resource.getResourceName())
.setNumberOfImages(numberOfImages)
.setBiggestFrameFootprintBytes(biggestFrameMemoryFootprint)
.setBounds(accumulatedBounds)
@@ -212,17 +205,8 @@ static Optional fromPackageFile(InputPackage.PackageFil
.build());
} catch (IOException e) {
throw new IllegalArgumentException(
- String.format("Error while processing image %s", packageFile.getFilePath()), e);
- }
- }
-
- private static String getResourceName(InputPackage.PackageFile packageFile) {
- String resourceNameWithExtension = packageFile.getFilePath().getFileName().toString();
- int dotIndex = resourceNameWithExtension.lastIndexOf('.');
- if (dotIndex != -1) {
- return resourceNameWithExtension.substring(0, dotIndex);
+ String.format("Error while processing image %s", resource.getFilePath()), e);
}
- return resourceNameWithExtension;
}
private final String name;
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/InputPackage.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/InputPackage.java
index 377e2e5..aaaa036 100644
--- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/InputPackage.java
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/InputPackage.java
@@ -16,20 +16,12 @@
package com.google.wear.watchface.dfx.memory;
-import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Iterator;
import java.util.Optional;
-import java.util.Spliterator;
-import java.util.Spliterators;
import java.util.regex.Pattern;
import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -40,36 +32,12 @@
* the package.
*/
interface InputPackage extends AutoCloseable {
-
- /** Represents a file in a watch face package (apk or aab). */
- class PackageFile {
-
- private final Path filePath;
-
- private final byte[] data;
-
- PackageFile(Path filePath, byte[] data) {
- this.filePath = filePath;
- this.data = data;
- }
-
- /** File path relative to the root of the declarative watch face package. */
- public Path getFilePath() {
- return filePath;
- }
-
- /** File raw data. */
- public byte[] getData() {
- return data;
- }
- }
-
/**
- * Returns a stream of file representations form the watch face package. The stream must not be
- * consumed more than once. The InputPackage must be closed only after consuming the stream of
- * files.
+ * Returns a stream of resource representations form the watch face package. The stream must not
+ * be consumed more than once. The InputPackage must be closed only after consuming the stream
+ * of files.
*/
- Stream getWatchFaceFiles();
+ Stream getWatchFaceFiles();
/** Close the backing watch face package resource. */
void close();
@@ -85,8 +53,10 @@ static InputPackage open(String packagePath) throws IOException {
return openFromAabDirectory(packageFile);
} else if (packagePath.endsWith("zip")) {
return openFromMokkaZip(packagePath);
- } else if (packagePath.endsWith("aab") || packagePath.endsWith("apk")) {
- return openFromAndroidPackage(packagePath);
+ } else if (packagePath.endsWith("aab")) {
+ return openFromAabFile(packagePath);
+ } else if (packagePath.endsWith("apk")) {
+ return openFromApkFile(packagePath);
} else {
throw new IllegalArgumentException("Incorrect file type");
}
@@ -96,31 +66,47 @@ static InputPackage open(String packagePath) throws IOException {
* Creates an input package from a directory containing the structure of a Declarative Watch
* Face AAB. Each file is relative to the base module of the directory.
*/
- static InputPackage openFromAabDirectory(File aabDirectory) throws IOException {
+ static InputPackage openFromAabDirectory(File aabDirectory) {
Path rootPath = aabDirectory.toPath();
- Stream childrenFilesStream = Files.walk(rootPath);
return new InputPackage() {
@Override
- public Stream getWatchFaceFiles() {
- return childrenFilesStream
- .filter(childPath -> childPath.toFile().isFile())
- .map(
- childPath -> {
- byte[] fileContent;
- try {
- fileContent = Files.readAllBytes(childPath);
- } catch (IOException e) {
- throw new RuntimeException(
- "Cannot read file " + childPath, e);
- }
- return new PackageFile(
- rootPath.relativize(childPath), fileContent);
- });
+ public Stream getWatchFaceFiles() {
+ try {
+ return AndroidResourceLoader.streamFromAabDirectory(rootPath);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
public void close() {
- childrenFilesStream.close();
+
+ }
+ };
+ }
+
+ /**
+ * Creates an input package from a declarative watch face APK.
+ */
+ static InputPackage openFromApkFile(String apkPath) throws IOException {
+ final ZipFile zipFile = new ZipFile(apkPath);
+ return new InputPackage() {
+ @Override
+ public Stream getWatchFaceFiles() {
+ try {
+ return AndroidResourceLoader.streamFromApkFile(zipFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ zipFile.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
};
}
@@ -129,22 +115,12 @@ public void close() {
* Creates an input package from a declarative watch face AAB. Each file is relative to the base
* module of the app bundle. Every other module will be ignored.
*/
- static InputPackage openFromAndroidPackage(String aabPath) throws IOException {
+ static InputPackage openFromAabFile(String aabPath) throws IOException {
final ZipFile zipFile = new ZipFile(aabPath);
return new InputPackage() {
@Override
- public Stream getWatchFaceFiles() {
- return zipFile.stream()
- .map(
- entry -> {
- byte[] fileData;
- try {
- fileData = readAllBytes(zipFile.getInputStream(entry));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return new PackageFile(Paths.get(entry.getName()), fileData);
- });
+ public Stream getWatchFaceFiles() {
+ return AndroidResourceLoader.streamFromAabFile(zipFile);
}
@Override
@@ -176,37 +152,15 @@ static InputPackage openFromMokkaZip(String zipPath) throws IOException {
}
ZipInputStream baseSplitApkZip =
new ZipInputStream(mokkaZip.getInputStream(baseSplitApk.get()));
- Iterator iterator =
- new Iterator() {
- private ZipEntry zipEntry;
-
- @Override
- public boolean hasNext() {
- try {
- zipEntry = baseSplitApkZip.getNextEntry();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return zipEntry != null;
- }
-
- @Override
- public PackageFile next() {
- byte[] entryData;
- try {
- entryData = readAllBytes(baseSplitApkZip);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return new PackageFile(Paths.get(zipEntry.getName()), entryData);
- }
- };
+
return new InputPackage() {
@Override
- public Stream getWatchFaceFiles() {
- return StreamSupport.stream(
- Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
- false);
+ public Stream getWatchFaceFiles() {
+ try {
+ return AndroidResourceLoader.streamFromMokkaZip(baseSplitApkZip);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
@@ -223,19 +177,4 @@ public void close() {
throw e;
}
}
-
- /** Read all bytes from an input stream to a new byte array. */
- static byte[] readAllBytes(InputStream inputStream) throws IOException {
- int len;
- byte[] buffer = new byte[1024];
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- while ((len = inputStream.read(buffer)) > 0) {
- bos.write(buffer, 0, len);
- }
- return bos.toByteArray();
- }
-
- static boolean pathMatchesGlob(Path path, String glob) {
- return path.getFileSystem().getPathMatcher("glob:" + glob).matches(path);
- }
}
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.java b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.java
index 56cba09..33dcae8 100644
--- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.java
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.java
@@ -16,10 +16,6 @@
package com.google.wear.watchface.dfx.memory;
-import static com.google.wear.watchface.dfx.memory.InputPackage.pathMatchesGlob;
-
-import com.google.wear.watchface.dfx.memory.InputPackage.PackageFile;
-
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
@@ -89,21 +85,21 @@ private void recordResourceDetails(DrawableResourceDetails resourceDetails) {
}
}
- /** Creates a WatchFaceData object from a stream of watch face package files. */
+ /** Creates a WatchFaceData object from a stream of watch face package resources. */
static WatchFaceData fromResourcesStream(
- Stream resources, EvaluationSettings evaluationSettings) {
+ Stream resources, EvaluationSettings evaluationSettings) {
WatchFaceData watchFaceData = new WatchFaceData();
resources.forEach(
resource -> {
- if (pathMatchesGlob(resource.getFilePath(), "**/raw/*.xml")) {
+ if (resource.isWatchFaceXml()) {
Document document = parseXmlResource(resource.getData());
if (isWatchFaceDocument(document, evaluationSettings)) {
watchFaceData.watchFaceDocuments.add(document);
return;
}
}
- DrawableResourceDetails.fromPackageFile(resource)
+ DrawableResourceDetails.fromPackageResource(resource)
.ifPresent(watchFaceData::recordResourceDetails);
});
diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetailsTest.java b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetailsTest.java
index 6d1ed7c..76fde44 100644
--- a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetailsTest.java
+++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/DrawableResourceDetailsTest.java
@@ -1,7 +1,6 @@
package com.google.wear.watchface.dfx.memory;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.wear.watchface.dfx.memory.DrawableResourceDetails.fromPackageFile;
import com.google.common.collect.ImmutableList;
import com.google.common.jimfs.Configuration;
@@ -11,7 +10,6 @@
import java.io.InputStream;
import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -43,10 +41,11 @@ public class DrawableResourceDetailsTest {
@Test
public void fromPackageFile_parsesTtfWithExtension() throws Exception {
- InputPackage.PackageFile ttfFile = readPackageFile("base/res/font/roboto_regular.ttf");
+
+ AndroidResource ttfFile = readPackageFile("base/res/font/roboto_regular.ttf");
Optional fontDetails =
- DrawableResourceDetails.fromPackageFile(ttfFile);
+ DrawableResourceDetails.fromPackageResource(ttfFile);
assertThat(fontDetails.isPresent()).isTrue();
assertThat(fontDetails.get()).isEqualTo(expectedRobotoRegular);
@@ -54,13 +53,14 @@ public void fromPackageFile_parsesTtfWithExtension() throws Exception {
@Test
public void fromPackageFile_parsesTtfWithoutExtension() throws Exception {
- InputPackage.PackageFile ttfFile =
+
+ AndroidResource ttfFile =
changePath(
readPackageFile("base/res/font/roboto_regular.ttf"),
Paths.get("base/res/font/roboto_regular"));
Optional fontDetails =
- DrawableResourceDetails.fromPackageFile(ttfFile);
+ DrawableResourceDetails.fromPackageResource(ttfFile);
assertThat(fontDetails.isPresent()).isTrue();
assertThat(fontDetails.get()).isEqualTo(expectedRobotoRegular);
@@ -68,10 +68,10 @@ public void fromPackageFile_parsesTtfWithoutExtension() throws Exception {
@Test
public void fromPackageFile_parsesPngWithExtension() throws Exception {
- InputPackage.PackageFile pngFile = readPackageFile("base/res/drawable-nodpi/dial.png");
+ AndroidResource pngFile = readPackageFile("base/res/drawable-nodpi/dial.png");
Optional pngDetails =
- DrawableResourceDetails.fromPackageFile(pngFile);
+ DrawableResourceDetails.fromPackageResource(pngFile);
assertThat(pngDetails.isPresent()).isTrue();
assertThat(pngDetails.get()).isEqualTo(expectedPng);
@@ -79,13 +79,13 @@ public void fromPackageFile_parsesPngWithExtension() throws Exception {
@Test
public void fromPackageFile_parsesPngWithoutExtension() throws Exception {
- InputPackage.PackageFile pngFile =
+ AndroidResource pngFile =
changePath(
readPackageFile("base/res/drawable-nodpi/dial.png"),
Paths.get("base/res/drawable-nodpi/dial"));
Optional pngDetails =
- DrawableResourceDetails.fromPackageFile(pngFile);
+ DrawableResourceDetails.fromPackageResource(pngFile);
assertThat(pngDetails.isPresent()).isTrue();
assertThat(pngDetails.get()).isEqualTo(expectedPng);
@@ -95,14 +95,14 @@ public void fromPackageFile_parsesPngWithoutExtension() throws Exception {
public void fromPackageFile_parsesPngFromDifferentQualifierDrawable() throws Exception {
ImmutableList testFilePaths =
ImmutableList.of("base/res/drawable-xhdpi/dial.png", "base/res/drawable/dial.png");
- InputPackage.PackageFile originalPackageFile =
+ AndroidResource originalPackageFile =
readPackageFile("base/res/drawable-nodpi/dial.png");
for (String testFilePath : testFilePaths) {
- InputPackage.PackageFile testPackageFile =
+ AndroidResource testPackageFile =
changePath(originalPackageFile, Paths.get(testFilePath));
Optional testDrawableDetails =
- DrawableResourceDetails.fromPackageFile(testPackageFile);
+ DrawableResourceDetails.fromPackageResource(testPackageFile);
assertThat(testDrawableDetails.isPresent()).isTrue();
}
@@ -116,14 +116,14 @@ public void fromPackageFile_parsesResourceFromApkRoot() throws Exception {
ImmutableList.of(
"base/res/drawable-nodpi/dial.png", "base/res/font/roboto_regular.ttf");
for (String testFilePath : testFilePaths) {
- InputPackage.PackageFile testPackageFile =
+ AndroidResource testPackageFile =
changePath(
readPackageFile(testFilePath),
// drop the module name from the paths
dropSections(Paths.get(testFilePath), 1));
Optional testDrawableDetails =
- DrawableResourceDetails.fromPackageFile(testPackageFile);
+ DrawableResourceDetails.fromPackageResource(testPackageFile);
assertThat(testDrawableDetails.isPresent()).isTrue();
}
@@ -135,16 +135,15 @@ public void fromPackageFile_parsesResourceFromWindowsPath() throws Exception {
// first component) and APK-style paths, where the module name does not exist
try (FileSystem windowsFileSystem = Jimfs.newFileSystem(Configuration.windows())) {
ImmutableList testFilePaths =
- ImmutableList.of(
- "base/res/drawable-nodpi/dial.png", "base/res/font/roboto_regular.ttf");
+ ImmutableList.of(
+ "base/res/font/roboto_regular.ttf", "base/res/drawable-nodpi/dial.png");
for (String testFilePath : testFilePaths) {
- InputPackage.PackageFile testPackageFile =
- changePath(
- readPackageFile(testFilePath),
- makeWindowsPath(testFilePath, windowsFileSystem));
+ AndroidResource testPackageFile =
+ changePath(readPackageFile(testFilePath),
+ makeWindowsPath(testFilePath, windowsFileSystem));
Optional testDrawableDetails =
- DrawableResourceDetails.fromPackageFile(testPackageFile);
+ DrawableResourceDetails.fromPackageResource(testPackageFile);
assertThat(testDrawableDetails.isPresent()).isTrue();
}
@@ -155,13 +154,13 @@ public void fromPackageFile_parsesResourceFromWindowsPath() throws Exception {
public void fromPackageFile_parsesResourceFromAssetsAndRaw() throws Exception {
ImmutableList testFilePaths =
ImmutableList.of("base/res/drawable-xhdpi/dial.png", "base/res/drawable/dial.png");
- InputPackage.PackageFile originalFile = readPackageFile("base/res/drawable-nodpi/dial.png");
+ AndroidResource originalFile = readPackageFile("base/res/drawable-nodpi/dial.png");
for (String testFilePath : testFilePaths) {
- InputPackage.PackageFile testPackageFile =
+ AndroidResource testPackageFile =
changePath(originalFile, Paths.get(testFilePath));
Optional testDrawableDetails =
- DrawableResourceDetails.fromPackageFile(testPackageFile);
+ DrawableResourceDetails.fromPackageResource(testPackageFile);
assertThat(testDrawableDetails.isPresent()).isTrue();
assertThat(testDrawableDetails.get()).isEqualTo(expectedPng);
@@ -171,24 +170,23 @@ public void fromPackageFile_parsesResourceFromAssetsAndRaw() throws Exception {
@Test
public void fromPackageFile_returnsNoneOnNonDrawableResourceFileUnderDrawables()
throws Exception {
- InputPackage.PackageFile unexpectedFile =
+ AndroidResource unexpectedFile =
changePath(
readPackageFile("base/res/xml/watch_face_info.xml"),
Paths.get("base/res/drawable/non-image"));
Optional resourceDetailsOptional =
- DrawableResourceDetails.fromPackageFile(unexpectedFile);
+ DrawableResourceDetails.fromPackageResource(unexpectedFile);
assertThat(resourceDetailsOptional.isPresent()).isFalse();
}
@Test
public void fromPackageFile_returnsNoneOnNonDrawableResourceFile() throws Exception {
- InputPackage.PackageFile unexpectedFile =
- readPackageFile("base/res/xml/watch_face_info.xml");
+ AndroidResource unexpectedFile = readPackageFile("base/res/xml/watch_face_info.xml");
Optional resourceDetailsOptional =
- DrawableResourceDetails.fromPackageFile(unexpectedFile);
+ DrawableResourceDetails.fromPackageResource(unexpectedFile);
assertThat(resourceDetailsOptional.isPresent()).isFalse();
}
@@ -215,14 +213,14 @@ public void canUseRGB565() throws Exception {
assertThat(eightBppNeeded.canUseRGB565()).isFalse();
}
- private InputPackage.PackageFile readPackageFile(String originFilePath) throws Exception {
+ private AndroidResource readPackageFile(String originFilePath) throws Exception {
Path filePath = Paths.get(TEST_PACKAGE_FILES_ROOT, originFilePath);
byte[] bytes = Files.readAllBytes(filePath);
- return new InputPackage.PackageFile(Paths.get(originFilePath), bytes);
+ return AndroidResource.fromPath(filePath, bytes);
}
- private InputPackage.PackageFile changePath(InputPackage.PackageFile origin, Path newPath) {
- return new InputPackage.PackageFile(newPath, origin.getData());
+ private AndroidResource changePath(AndroidResource origin, Path newPath) {
+ return AndroidResource.fromPath(newPath, origin.getData());
}
private Path dropSections(Path path, int count) {
@@ -238,10 +236,9 @@ private Path makeWindowsPath(String originalPath, FileSystem fileSystem) {
Optional getDrawableResourceDetails(String name) throws Exception {
String path = String.format("/res/drawable/%s", name);
try (InputStream is = getClass().getResourceAsStream(path)) {
- return fromPackageFile(
- new InputPackage.PackageFile(
- FileSystems.getDefault().getPath("res", "drawable", name),
- InputPackage.readAllBytes(is)));
+ return DrawableResourceDetails.fromPackageResource(
+ AndroidResource.fromPath(path, AndroidResourceLoader.readAllBytes(is))
+ );
}
}
}
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 6e3f234..d841ff9 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.truth.Correspondence;
-import com.google.wear.watchface.dfx.memory.InputPackage.PackageFile;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -16,7 +15,7 @@
@RunWith(JUnit4.class)
public class InputPackageTest {
- private static final Correspondence VERIFY_PACKAGE_NAME_ONLY =
+ private static final Correspondence VERIFY_PACKAGE_NAME_ONLY =
Correspondence.transforming(
packageFile -> packageFile.getFilePath().toString(),
"has the same file path as");
@@ -29,7 +28,7 @@ public void open_handlesFolder() throws Exception {
.toAbsolutePath()
.toString();
- List packageFiles;
+ List packageFiles;
try (InputPackage inputPackage = InputPackage.open(testAabDirectory)) {
packageFiles =
inputPackage
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 5bc94b8..95783f8 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
@@ -5,7 +5,6 @@
import static com.google.wear.watchface.dfx.memory.ResourceMemoryEvaluator.evaluateWatchFaceForLayout;
import static com.google.wear.watchface.dfx.memory.WatchFaceData.SYSTEM_DEFAULT_FONT;
import static com.google.wear.watchface.dfx.memory.WatchFaceData.SYSTEM_DEFAULT_FONT_SIZE;
-
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertThrows;
@@ -88,6 +87,7 @@ public static Collection