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 data() { return Stream.of( "unpackedBundle/release", "apk/release/sample-wf-release.apk", + "apk/debug/sample-wf-debug.apk", "bundle/release/sample-wf-release.aab", "zipApk/com.google.wear.watchface.memory.sample.zip") .map( diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java index dc4c39b..abd1fbc 100644 --- a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java +++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java @@ -61,7 +61,7 @@ public class WatchFaceDataTest { @Test public void fromResourcesStream_createsPackageFromLinuxPaths() { - Stream packageFileStream = + Stream packageFileStream = TEST_FILES.stream().map(this::readPackageFile); WatchFaceData watchFaceData = @@ -74,7 +74,7 @@ public void fromResourcesStream_createsPackageFromLinuxPaths() { @Test public void fromResourcesStream_createsPackageFromWindowsPaths() throws Exception { try (FileSystem windowsFs = Jimfs.newFileSystem(Configuration.windows())) { - Stream packageFileStream = + Stream packageFileStream = TEST_FILES.stream().map(x -> readWindowsPackageFile(x, windowsFs)); WatchFaceData watchFaceData = @@ -85,7 +85,7 @@ public void fromResourcesStream_createsPackageFromWindowsPaths() throws Exceptio } } - private InputPackage.PackageFile readPackageFile(String path) throws RuntimeException { + private AndroidResource readPackageFile(String path) throws RuntimeException { Path rootPath = Paths.get(TEST_PACKAGE_FILES_ROOT); Path filePath = Paths.get(TEST_PACKAGE_FILES_ROOT, path); byte[] bytes; @@ -94,10 +94,12 @@ private InputPackage.PackageFile readPackageFile(String path) throws RuntimeExce } catch (IOException e) { throw new RuntimeException(e); } - return new InputPackage.PackageFile(rootPath.relativize(filePath), bytes); + return AndroidResource.fromPath( + rootPath.relativize(filePath), + bytes); } - private InputPackage.PackageFile readWindowsPackageFile(String path, FileSystem fileSystem) { + private AndroidResource readWindowsPackageFile(String path, FileSystem fileSystem) { Path filePath = Paths.get(TEST_PACKAGE_FILES_ROOT, path); byte[] bytes; try { @@ -109,6 +111,8 @@ private InputPackage.PackageFile readWindowsPackageFile(String path, FileSystem Path windowsPath = fileSystem.getPath( pathSplits[0], Arrays.copyOfRange(pathSplits, 1, pathSplits.length)); - return new InputPackage.PackageFile(windowsPath, bytes); + return AndroidResource.fromPath( + windowsPath, + bytes); } } diff --git a/play-validations/memory-footprint/test-samples/sample-wf/build.gradle b/play-validations/memory-footprint/test-samples/sample-wf/build.gradle index 437a6d5..9f95898 100644 --- a/play-validations/memory-footprint/test-samples/sample-wf/build.gradle +++ b/play-validations/memory-footprint/test-samples/sample-wf/build.gradle @@ -54,6 +54,10 @@ afterEvaluate { // create a zip that follows the structure used internally, for tests tasks.register('zipApk', Zip) { + duplicatesStrategy DuplicatesStrategy.INCLUDE + dependsOn 'assembleDebug' + from 'build/outputs/apk/debug' + include 'sample-wf-debug.apk' dependsOn 'assembleRelease' from 'build/outputs/apk/release' include 'sample-wf-release.apk'