Reapply "Initial setup for doing legacy resource shrinking in R8"
This reverts commit 8ea20ac4540f3800f13f5f54c52d3456bf9409fe.
Bug: 305892375
Bug: 287398085
Change-Id: If6e6ee396cd6dcee4619b733d94f6dd456e4b7b6
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
index 1136085..153f1ce 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceShrinkerImpl.kt
@@ -301,6 +301,11 @@
.any { it.isReachable }
}
+fun ResourceStore.isJarPathReachable(path: String) : Boolean {
+ val (_, folder, name) = path.split('/', limit = 3)
+ return isJarPathReachable(folder, name);
+}
+
private fun ResourceStore.getResourceId(
folder: String,
name: String
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
index 130e585..a0a1f76 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/graph/ProtoResourcesGraphBuilder.kt
@@ -21,6 +21,7 @@
import com.android.aapt.Resources.FileReference
import com.android.aapt.Resources.FileReference.Type.PROTO_XML
import com.android.aapt.Resources.Reference
+import com.android.aapt.Resources.ResourceTable
import com.android.aapt.Resources.XmlAttribute
import com.android.aapt.Resources.XmlElement
import com.android.aapt.Resources.XmlNode
@@ -56,12 +57,23 @@
* @param resourceTable path to resource table in proto format.
*/
class ProtoResourcesGraphBuilder(
- private val resourceRoot: Path,
- private val resourceTable: Path
+ private val resourceRoot: ResFolderFileTree,
+ private val resourceTableProducer: (ResourceShrinkerModel) -> ResourceTable
) : ResourcesGraphBuilder {
+ constructor(resourceRootPath: Path, resourceTablePath: Path) : this(
+ object : ResFolderFileTree {
+ override fun getEntryByName(pathInRes: String): ByteArray {
+ val lazyVal : ByteArray by lazy {
+ Files.readAllBytes(resourceRootPath.resolve(pathInRes))
+ }
+ return lazyVal
+ }
+ },
+ { model -> model.readResourceTable(resourceTablePath) }
+ )
override fun buildGraph(model: ResourceShrinkerModel) {
- model.readResourceTable(resourceTable).entriesSequence()
+ resourceTableProducer(model).entriesSequence()
.map { (id, _, _, entry) ->
model.resourceStore.getResource(id)?.let {
ReferencesForResourceFinder(resourceRoot, model, entry, it)
@@ -71,9 +83,12 @@
.forEach { it.findReferences() }
}
}
+interface ResFolderFileTree {
+ fun getEntryByName(pathInRes: String) : ByteArray
+}
private class ReferencesForResourceFinder(
- private val resourcesRoot: Path,
+ private val resourcesRoot: ResFolderFileTree,
private val model: ResourceShrinkerModel,
private val entry: Entry,
private val current: Resource
@@ -196,10 +211,9 @@
}
private fun findFromFile(file: FileReference) {
- val path = resourcesRoot.resolve(file.path.substringAfter("res/"))
- val bytes: ByteArray by lazy { Files.readAllBytes(path) }
+ val bytes = resourcesRoot.getEntryByName(file.path.substringAfter("res/"))
val content: String by lazy { String(bytes, StandardCharsets.UTF_8) }
- val extension = Ascii.toLowerCase(path.fileName.toString()).substringAfter('.')
+ val extension = Ascii.toLowerCase(file.path.substringAfterLast('.'))
when {
file.type == PROTO_XML -> fillFromXmlNode(XmlNode.parseFrom(bytes))
extension in listOf("html", "htm") -> webTokenizers.tokenizeHtml(content)
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
new file mode 100644
index 0000000..9fd4163
--- /dev/null
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -0,0 +1,270 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.build.shrinker.r8integration;
+
+import static java.nio.charset.StandardCharsets.UTF_16BE;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.aapt.Resources.ResourceTable;
+import com.android.aapt.Resources.XmlNode;
+import com.android.build.shrinker.NoDebugReporter;
+import com.android.build.shrinker.ResourceShrinkerImplKt;
+import com.android.build.shrinker.ResourceTableUtilKt;
+import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
+import com.android.build.shrinker.graph.ResFolderFileTree;
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
+import com.android.build.shrinker.usages.DexFileAnalysisCallback;
+import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
+import com.android.build.shrinker.usages.R8ResourceShrinker;
+import com.android.build.shrinker.usages.ToolsAttributeUsageRecorderKt;
+import com.android.ide.common.resources.usage.ResourceStore;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.xml.parsers.ParserConfigurationException;
+import org.jetbrains.annotations.NotNull;
+import org.xml.sax.SAXException;
+
+public class LegacyResourceShrinker {
+ private final Map<Integer, byte[]> dexInputs;
+ private final List<PathAndBytes> resFolderInputs;
+ private final List<PathAndBytes> xmlInputs;
+ private final PathAndBytes manifest;
+ private final PathAndBytes resourceTable;
+
+ public static class Builder {
+
+ private final Map<Integer, byte[]> dexInputs = new HashMap<>();
+ private final List<PathAndBytes> resFolderInputs = new ArrayList<>();
+ private final List<PathAndBytes> xmlInputs = new ArrayList<>();
+
+ private PathAndBytes manifest;
+ private PathAndBytes resourceTable;
+
+ private Builder() {}
+
+ public Builder setManifest(Path path, byte[] bytes) {
+ this.manifest = new PathAndBytes(bytes, path);
+ return this;
+ }
+
+ public Builder setResourceTable(Path path, byte[] bytes) {
+ this.resourceTable = new PathAndBytes(bytes, path);
+ return this;
+ }
+
+ public Builder addDexInput(int index, byte[] bytes) {
+ dexInputs.put(index, bytes);
+ return this;
+ }
+
+ public Builder addResFolderInput(Path path, byte[] bytes) {
+ resFolderInputs.add(new PathAndBytes(bytes, path));
+ return this;
+ }
+
+ public Builder addXmlInput(Path path, byte[] bytes) {
+ xmlInputs.add(new PathAndBytes(bytes, path));
+ return this;
+ }
+
+ public LegacyResourceShrinker build() {
+ assert manifest != null && resourceTable != null;
+ return new LegacyResourceShrinker(
+ dexInputs, resFolderInputs, manifest, resourceTable, xmlInputs);
+ }
+ }
+
+ private LegacyResourceShrinker(
+ Map<Integer, byte[]> dexInputs,
+ List<PathAndBytes> resFolderInputs,
+ PathAndBytes manifest,
+ PathAndBytes resourceTable,
+ List<PathAndBytes> xmlInputs) {
+ this.dexInputs = dexInputs;
+ this.resFolderInputs = resFolderInputs;
+ this.manifest = manifest;
+ this.resourceTable = resourceTable;
+ this.xmlInputs = xmlInputs;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public ShrinkerResult run() throws IOException, ParserConfigurationException, SAXException {
+ R8ResourceShrinkerModel model = new R8ResourceShrinkerModel(NoDebugReporter.INSTANCE, false);
+ ResourceTable loadedResourceTable = ResourceTable.parseFrom(resourceTable.bytes);
+ model.instantiateFromResourceTable(loadedResourceTable);
+ for (Entry<Integer, byte[]> entry : dexInputs.entrySet()) {
+ // The analysis needs an origin for the dex files, synthesize an easy recognizable one.
+ Path inMemoryR8 = Paths.get("in_memory_r8_classes" + entry.getKey() + ".dex");
+ R8ResourceShrinker.runResourceShrinkerAnalysis(
+ entry.getValue(), inMemoryR8, new DexFileAnalysisCallback(inMemoryR8, model));
+ }
+ ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(
+ XmlNode.parseFrom(manifest.bytes), model);
+ for (PathAndBytes xmlInput : xmlInputs) {
+ ToolsAttributeUsageRecorderKt.processRawXml(getUtfReader(xmlInput.getBytes()), model);
+ }
+ new ProtoResourcesGraphBuilder(
+ new ResFolderFileTree() {
+ Map<String, PathAndBytes> pathToBytes =
+ new ImmutableMap.Builder<String, PathAndBytes>()
+ .putAll(
+ xmlInputs.stream()
+ .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+ .putAll(
+ resFolderInputs.stream()
+ .collect(Collectors.toMap(PathAndBytes::getPathWithoutRes, a -> a)))
+ .build();
+
+ @Override
+ public byte[] getEntryByName(@NotNull String pathInRes) {
+ return pathToBytes.get(pathInRes).getBytes();
+ }
+ },
+ unused -> loadedResourceTable)
+ .buildGraph(model);
+ ResourceStore resourceStore = model.getResourceStore();
+ resourceStore.processToolsAttributes();
+ model.keepPossiblyReferencedResources();
+ ImmutableSet.Builder<String> resEntriesToKeep = new ImmutableSet.Builder<>();
+ for (PathAndBytes xmlInput : Iterables.concat(xmlInputs, resFolderInputs)) {
+ if (ResourceShrinkerImplKt.isJarPathReachable(resourceStore, xmlInput.path.toString())) {
+ resEntriesToKeep.add(xmlInput.path.toString());
+ }
+ }
+ List<Integer> resourceIdsToRemove =
+ model.getResourceStore().getResources().stream()
+ .filter(r -> !r.isReachable())
+ .map(r -> r.value)
+ .collect(Collectors.toList());
+ ResourceTable shrunkenResourceTable =
+ ResourceTableUtilKt.nullOutEntriesWithIds(loadedResourceTable, resourceIdsToRemove);
+ return new ShrinkerResult(resEntriesToKeep.build(), shrunkenResourceTable.toByteArray());
+ }
+
+ // Lifted from com/android/utils/XmlUtils.java which we can't easily update internal dependency
+ // for.
+ /**
+ * Returns a character reader for the given bytes, which must be a UTF encoded file.
+ *
+ * <p>The reader does not need to be closed by the caller (because the file is read in full in one
+ * shot and the resulting array is then wrapped in a byte array input stream, which does not need
+ * to be closed.)
+ */
+ public static Reader getUtfReader(byte[] bytes) throws IOException {
+ int length = bytes.length;
+ if (length == 0) {
+ return new StringReader("");
+ }
+
+ switch (bytes[0]) {
+ case (byte) 0xEF:
+ {
+ if (length >= 3 && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
+ // UTF-8 BOM: EF BB BF: Skip it
+ return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3), UTF_8);
+ }
+ break;
+ }
+ case (byte) 0xFE:
+ {
+ if (length >= 2 && bytes[1] == (byte) 0xFF) {
+ // UTF-16 Big Endian BOM: FE FF
+ return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16BE);
+ }
+ break;
+ }
+ case (byte) 0xFF:
+ {
+ if (length >= 2 && bytes[1] == (byte) 0xFE) {
+ if (length >= 4 && bytes[2] == (byte) 0x00 && bytes[3] == (byte) 0x00) {
+ // UTF-32 Little Endian BOM: FF FE 00 00
+ return new InputStreamReader(
+ new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32LE");
+ }
+
+ // UTF-16 Little Endian BOM: FF FE
+ return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2), UTF_16LE);
+ }
+ break;
+ }
+ case (byte) 0x00:
+ {
+ if (length >= 4
+ && bytes[0] == (byte) 0x00
+ && bytes[1] == (byte) 0x00
+ && bytes[2] == (byte) 0xFE
+ && bytes[3] == (byte) 0xFF) {
+ // UTF-32 Big Endian BOM: 00 00 FE FF
+ return new InputStreamReader(
+ new ByteArrayInputStream(bytes, 4, length - 4), "UTF-32BE");
+ }
+ break;
+ }
+ }
+
+ // No byte order mark: Assume UTF-8 (where the BOM is optional).
+ return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8);
+ }
+
+ public static class ShrinkerResult {
+ private final Set<String> resFolderEntriesToKeep;
+ private final byte[] resourceTableInProtoFormat;
+
+ public ShrinkerResult(Set<String> resFolderEntriesToKeep, byte[] resourceTableInProtoFormat) {
+ this.resFolderEntriesToKeep = resFolderEntriesToKeep;
+ this.resourceTableInProtoFormat = resourceTableInProtoFormat;
+ }
+
+ public byte[] getResourceTableInProtoFormat() {
+ return resourceTableInProtoFormat;
+ }
+
+ public Set<String> getResFolderEntriesToKeep() {
+ return resFolderEntriesToKeep;
+ }
+ }
+
+ private static class PathAndBytes {
+ private final byte[] bytes;
+ private final Path path;
+
+ private PathAndBytes(byte[] bytes, Path path) {
+ this.bytes = bytes;
+ this.path = path;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public String getPathWithoutRes() {
+ assert path.toString().startsWith("res/");
+ return path.toString().substring(4);
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+ }
+}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 5f9bec6..d8ac6a9 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -32,7 +32,7 @@
r8ResourceShrinkerModel.instantiateFromResourceTable(inputStream);
}
- private static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
+ public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
public R8ResourceShrinkerModel(
ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
@@ -40,25 +40,29 @@
}
// Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
- public void instantiateFromResourceTable(InputStream inputStream) {
+ void instantiateFromResourceTable(InputStream inputStream) {
try {
ResourceTable resourceTable = ResourceTable.parseFrom(inputStream);
- ResourceTableUtilKt.entriesSequence(resourceTable)
- .iterator()
- .forEachRemaining(
- entryWrapper -> {
- ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
- if (resourceType != ResourceType.STYLEABLE) {
- this.addResource(
- resourceType,
- entryWrapper.getPackageName(),
- ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
- entryWrapper.getId());
- }
- });
+ instantiateFromResourceTable(resourceTable);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
+
+ void instantiateFromResourceTable(ResourceTable resourceTable) {
+ ResourceTableUtilKt.entriesSequence(resourceTable)
+ .iterator()
+ .forEachRemaining(
+ entryWrapper -> {
+ ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
+ if (resourceType != ResourceType.STYLEABLE) {
+ this.addResource(
+ resourceType,
+ entryWrapper.getPackageName(),
+ ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
+ entryWrapper.getId());
+ }
+ });
+ }
}
}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
index 3f1a676..5d6a0df 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/DexUsageRecorder.kt
@@ -56,7 +56,7 @@
}
}
-private class DexFileAnalysisCallback(
+class DexFileAnalysisCallback(
private val path: Path,
private val model: ResourceShrinkerModel
) : AnalysisCallback {
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
index 4e6aa3e..77bb8f6 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ProtoAndroidManifestUsageRecorder.kt
@@ -34,25 +34,25 @@
recordUsagesFromNode(root, model)
}
- private fun recordUsagesFromNode(node: XmlNode, model: ResourceShrinkerModel) {
- // Records only resources from element attributes that have reference items with resolved
- // ids or names.
- if (!node.hasElement()) {
- return
- }
- node.element.attributeList.asSequence()
- .filter { it.hasCompiledItem() }
- .map { it.compiledItem }
- .filter { it.hasRef() }
- .map { it.ref }
- .flatMap {
- // If resource id is available prefer this id to name.
- when {
- it.id != 0 -> listOfNotNull(model.resourceStore.getResource(it.id))
- else -> model.resourceStore.getResourcesFromUrl("@${it.name}")
- }.asSequence()
- }
- .forEach { ResourceUsageModel.markReachable(it) }
- node.element.childList.forEach { recordUsagesFromNode(it, model) }
+}
+fun recordUsagesFromNode(node: XmlNode, model: ResourceShrinkerModel) {
+ // Records only resources from element attributes that have reference items with resolved
+ // ids or names.
+ if (!node.hasElement()) {
+ return
}
+ node.element.attributeList.asSequence()
+ .filter { it.hasCompiledItem() }
+ .map { it.compiledItem }
+ .filter { it.hasRef() }
+ .map { it.ref }
+ .flatMap {
+ // If resource id is available prefer this id to name.
+ when {
+ it.id != 0 -> listOfNotNull(model.resourceStore.getResource(it.id))
+ else -> model.resourceStore.getResourcesFromUrl("@${it.name}")
+ }.asSequence()
+ }
+ .forEach { ResourceUsageModel.markReachable(it) }
+ node.element.childList.forEach { recordUsagesFromNode(it, model) }
}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
index 0dae39a..8e43cd8 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/usages/ToolsAttributeUsageRecorder.kt
@@ -16,9 +16,11 @@
package com.android.build.shrinker.usages
+import com.android.SdkConstants.TOOLS_NS_NAME
import com.android.SdkConstants.VALUE_STRICT
import com.android.build.shrinker.ResourceShrinkerModel
import com.android.utils.XmlUtils
+import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableMap.copyOf
import java.io.Reader
import java.nio.file.Files
@@ -37,9 +39,6 @@
* @param rawResourcesPath path to folder with resources in raw format.
*/
class ToolsAttributeUsageRecorder(val rawResourcesPath: Path) : ResourceUsageRecorder {
- companion object {
- private val TOOLS_NAMESPACE = "http://schemas.android.com/tools"
- }
override fun recordUsages(model: ResourceShrinkerModel) {
Files.walk(rawResourcesPath)
@@ -48,42 +47,48 @@
}
private fun processRawXml(path: Path, model: ResourceShrinkerModel) {
- processResourceToolsAttributes(path).forEach { key, value ->
- when (key) {
- "keep" -> model.resourceStore.recordKeepToolAttribute(value)
- "discard" -> model.resourceStore.recordDiscardToolAttribute(value)
- "shrinkMode" ->
- if (value == VALUE_STRICT) {
- model.resourceStore.safeMode = false
- }
- }
- }
- }
-
- private fun processResourceToolsAttributes(path: Path): Map<String, String> {
- val toolsAttributes = mutableMapOf<String, String>()
- XmlUtils.getUtfReader(path.toFile()).use { reader: Reader ->
- val factory = XMLInputFactory.newInstance()
- val xmlStreamReader = factory.createXMLStreamReader(reader)
-
- var rootElementProcessed = false
- while (!rootElementProcessed && xmlStreamReader.hasNext()) {
- xmlStreamReader.next()
- if (xmlStreamReader.isStartElement) {
- if (xmlStreamReader.localName == "resources") {
- for (i in 0 until xmlStreamReader.attributeCount) {
- if (xmlStreamReader.getAttributeNamespace(i) == TOOLS_NAMESPACE) {
- toolsAttributes.put(
- xmlStreamReader.getAttributeLocalName(i),
- xmlStreamReader.getAttributeValue(i)
- )
- }
- }
- }
- rootElementProcessed = true
- }
- }
- }
- return copyOf(toolsAttributes)
+ processRawXml(XmlUtils.getUtfReader(path.toFile()), model)
}
}
+
+fun processRawXml(reader: Reader, model: ResourceShrinkerModel) {
+ processResourceToolsAttributes(reader).forEach { key, value ->
+ when (key) {
+ "keep" -> model.resourceStore.recordKeepToolAttribute(value)
+ "discard" -> model.resourceStore.recordDiscardToolAttribute(value)
+ "shrinkMode" ->
+ if (value == VALUE_STRICT) {
+ model.resourceStore.safeMode = false
+ }
+ }
+ }
+}
+
+fun processResourceToolsAttributes(utfReader: Reader?): ImmutableMap<String, String> {
+ val toolsAttributes = mutableMapOf<String, String>()
+ utfReader.use { reader: Reader? ->
+ val factory = XMLInputFactory.newInstance()
+ val xmlStreamReader = factory.createXMLStreamReader(reader)
+
+ var rootElementProcessed = false
+ while (!rootElementProcessed && xmlStreamReader.hasNext()) {
+ xmlStreamReader.next()
+ if (xmlStreamReader.isStartElement) {
+ if (xmlStreamReader.localName == "resources") {
+ for (i in 0 until xmlStreamReader.attributeCount) {
+ val namespace = "http://schemas.android.com/tools"
+ if (xmlStreamReader.getAttributeNamespace(i) == namespace) {
+ toolsAttributes.put(
+ xmlStreamReader.getAttributeLocalName(i),
+ xmlStreamReader.getAttributeValue(i)
+ )
+ }
+ }
+ }
+ rootElementProcessed = true
+ }
+ }
+ }
+ return copyOf(toolsAttributes)
+}
+