| // Copyright 2018 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.android.aapt2; |
| |
| import static com.google.common.base.Predicates.not; |
| import static java.util.stream.Collectors.joining; |
| |
| import com.android.aapt.Resources; |
| import com.android.aapt.Resources.Array; |
| import com.android.aapt.Resources.Attribute.Symbol; |
| import com.android.aapt.Resources.CompoundValue; |
| import com.android.aapt.Resources.ConfigValue; |
| import com.android.aapt.Resources.Entry; |
| import com.android.aapt.Resources.FileReference; |
| import com.android.aapt.Resources.Item; |
| import com.android.aapt.Resources.Package; |
| import com.android.aapt.Resources.Plural; |
| import com.android.aapt.Resources.Reference; |
| import com.android.aapt.Resources.ResourceTable; |
| import com.android.aapt.Resources.Style; |
| import com.android.aapt.Resources.Type; |
| import com.android.aapt.Resources.Value; |
| import com.android.aapt.Resources.XmlAttribute; |
| import com.android.aapt.Resources.XmlElement; |
| import com.android.aapt.Resources.XmlNamespace; |
| import com.android.aapt.Resources.XmlNode; |
| import com.android.resources.ResourceType; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.xml.XmlEscapers; |
| import com.google.devtools.build.android.AndroidResourceOutputs.UniqueZipBuilder; |
| import com.google.protobuf.ByteString; |
| import com.google.protobuf.ExtensionRegistry; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URI; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.charset.StandardCharsets; |
| 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; |
| import java.nio.file.StandardOpenOption; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Deque; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.BiPredicate; |
| import java.util.function.Consumer; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Provides an interface to an apk in proto format. Since the apk is backed by a zip, it is |
| * important to close the ProtoApk when done. |
| */ |
| public class ProtoApk implements Closeable { |
| |
| static final Logger logger = Logger.getLogger(ProtoApk.class.getName()); |
| private static final String RESOURCE_TABLE = "resources.pb"; |
| private static final String MANIFEST = "AndroidManifest.xml"; |
| |
| private static final String RES_DIRECTORY = "res"; |
| |
| private final URI uri; |
| private final FileSystem apkFileSystem; |
| |
| private ProtoApk(URI uri, FileSystem apkFileSystem) { |
| this.uri = uri; |
| this.apkFileSystem = apkFileSystem; |
| } |
| |
| /** Reads a ProtoApk from a path and verifies that it is in the expected format. */ |
| public static ProtoApk readFrom(Path apkPath) throws IOException { |
| final URI uri = URI.create("jar:" + apkPath.toUri()); |
| return readFrom(uri); |
| } |
| |
| private static ProtoApk readFrom(URI uri) throws IOException { |
| final FileSystem apkFileSystem = FileSystems.newFileSystem(uri, ImmutableMap.of()); |
| Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(RESOURCE_TABLE))); |
| Preconditions.checkArgument(Files.exists(apkFileSystem.getPath(MANIFEST))); |
| return new ProtoApk(URI.create(uri.getSchemeSpecificPart()), apkFileSystem); |
| } |
| |
| /** |
| * Creates a copy of the current apk. |
| * |
| * @param destination Path to the new apk destination. |
| * @param resourceFilter A filter for determining whether a given resource will be included in the |
| * copy. |
| * @return The new ProtoApk. |
| * @throws IOException when there are issues reading the apk. |
| */ |
| public ProtoApk copy(Path destination, BiPredicate<ResourceType, String> resourceFilter) |
| throws IOException { |
| |
| final URI dstZipUri = URI.create("jar:" + destination.toUri()); |
| try (final ZipFile srcZip = new ZipFile(uri.getPath()); |
| final UniqueZipBuilder dstZip = UniqueZipBuilder.createFor(destination)) { |
| final ResourceTable.Builder dstTableBuilder = ResourceTable.newBuilder(); |
| final ResourceTable resourceTable = |
| ResourceTable.parseFrom( |
| Files.newInputStream(apkFileSystem.getPath(RESOURCE_TABLE)), |
| ExtensionRegistry.getEmptyRegistry()); |
| dstTableBuilder.setSourcePool(resourceTable.getSourcePool()); |
| for (Package pkg : resourceTable.getPackageList()) { |
| Package dstPkg = copyPackage(resourceFilter, dstZip, pkg); |
| dstTableBuilder.addPackage(dstPkg); |
| } |
| dstZip.addEntry(RESOURCE_TABLE, dstTableBuilder.build().toByteArray(), ZipEntry.DEFLATED); |
| srcZip.stream() |
| .filter(not(ZipEntry::isDirectory)) |
| .filter(entry -> !entry.getName().startsWith(RES_DIRECTORY + "/")) |
| .filter(entry -> !entry.getName().equals(RESOURCE_TABLE)) |
| .forEach( |
| entry -> { |
| try { |
| createDirectories(dstZip, apkFileSystem.getPath(entry.getName()).getParent()); |
| try (InputStream srcEntryInputStream = srcZip.getInputStream(entry)) { |
| byte[] content = ByteStreams.toByteArray(srcEntryInputStream); |
| dstZip.addEntry(entry, content); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| }); |
| } |
| |
| return readFrom(dstZipUri); |
| } |
| |
| /** |
| * Recursively creates all parent directories in {@code zip} before creating {@code directory}, |
| * similar to {@link Files#createDirectories}. |
| */ |
| private static void createDirectories(UniqueZipBuilder zip, @Nullable Path directory) |
| throws IOException { |
| if (directory == null) { |
| return; |
| } |
| createDirectories(zip, directory.getParent()); |
| zip.addDirEntry(directory.toString()); |
| } |
| |
| private Package copyPackage( |
| BiPredicate<ResourceType, String> resourceFilter, UniqueZipBuilder dstZip, Package pkg) |
| throws IOException { |
| Package.Builder dstPkgBuilder = Package.newBuilder(pkg); |
| dstPkgBuilder.clearType(); |
| for (Resources.Type type : pkg.getTypeList()) { |
| copyResourceType(resourceFilter, dstZip, dstPkgBuilder, type); |
| } |
| return dstPkgBuilder.build(); |
| } |
| |
| private void copyResourceType( |
| BiPredicate<ResourceType, String> resourceFilter, |
| UniqueZipBuilder dstZip, |
| Package.Builder dstPkgBuilder, |
| Resources.Type type) |
| throws IOException { |
| Type.Builder dstTypeBuilder = Resources.Type.newBuilder(type); |
| dstTypeBuilder.clearEntry(); |
| |
| ResourceType resourceType = ResourceType.getEnum(type.getName()); |
| for (Entry entry : type.getEntryList()) { |
| if (resourceFilter.test(resourceType, entry.getName())) { |
| copyEntry(dstZip, dstTypeBuilder, entry); |
| } |
| } |
| final Resources.Type dstType = dstTypeBuilder.build(); |
| if (!dstType.getEntryList().isEmpty()) { |
| dstPkgBuilder.addType(dstType); |
| } |
| } |
| |
| private void copyEntry(UniqueZipBuilder dstZip, Type.Builder dstTypeBuilder, Entry entry) |
| throws IOException { |
| dstTypeBuilder.addEntry(Entry.newBuilder(entry)); |
| for (ConfigValue configValue : entry.getConfigValueList()) { |
| if (configValue.hasValue() |
| && configValue.getValue().hasItem() |
| && configValue.getValue().getItem().hasFile()) { |
| final String path = configValue.getValue().getItem().getFile().getPath(); |
| final Path apkFileSystemPath = apkFileSystem.getPath(path); |
| createDirectories(dstZip, apkFileSystemPath.getParent()); |
| byte[] content = Files.readAllBytes(apkFileSystemPath); |
| dstZip.addEntry(path, content, ZipEntry.STORED); |
| } |
| } |
| } |
| |
| public XmlNode getManifest() throws IOException { |
| try (InputStream in = Files.newInputStream(apkFileSystem.getPath(MANIFEST))) { |
| return XmlNode.parseFrom(in, ExtensionRegistry.getEmptyRegistry()); |
| } |
| } |
| |
| /** Copy manifest as xml to an external directory. */ |
| public Path writeManifestAsXmlTo(Path directory) { |
| try (XmlWriter out = XmlWriter.openNew(Files.createDirectories(directory).resolve(MANIFEST))) { |
| out.write(getManifest()); |
| return directory.resolve(MANIFEST); |
| } catch (IOException e) { |
| throw new ProtoApkException(e); |
| } |
| } |
| |
| /** The apk as path. */ |
| public Path asApkPath() { |
| return Paths.get(uri); |
| } |
| |
| /** Thrown when errors occur during proto apk processing. */ |
| public static class ProtoApkException extends Aapt2Exception { |
| ProtoApkException(IOException e) { |
| super(e); |
| } |
| } |
| |
| private static class XmlWriter implements AutoCloseable { |
| static final ByteString ANGLE_OPEN = ByteString.copyFrom("<".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString SPACE = ByteString.copyFrom(" ".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString ANGLE_CLOSE = ByteString.copyFrom(">".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString FORWARD_SLASH = |
| ByteString.copyFrom("/".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString XMLNS = ByteString.copyFrom("xmlns:".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString EQUALS = ByteString.copyFrom("=".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString QUOTE = ByteString.copyFrom("\"".getBytes(StandardCharsets.UTF_8)); |
| static final ByteString COLON = ByteString.copyFrom(":".getBytes(StandardCharsets.UTF_8)); |
| private static final ByteString XML_PRELUDE = |
| ByteString.copyFrom( |
| "<?xml version=\"1.0\" encoding=\"utf-8\"?>".getBytes(StandardCharsets.UTF_8)); |
| |
| private final OutputStream out; |
| private final Deque<Map<ByteString, ByteString>> namespaceStack; |
| |
| static XmlWriter openNew(Path destination) throws IOException { |
| return new XmlWriter(Files.newOutputStream(destination, StandardOpenOption.CREATE_NEW)); |
| } |
| |
| private XmlWriter(OutputStream out) { |
| this.out = out; |
| this.namespaceStack = new ArrayDeque<>(); |
| } |
| |
| public void write(XmlNode node) throws IOException { |
| XML_PRELUDE.writeTo(out); |
| writeXmlFrom(node); |
| } |
| |
| private void writeXmlFrom(XmlNode node) throws IOException { |
| if (node.hasElement()) { |
| writeXmlFrom(node.getElement()); |
| } else { |
| out.write(node.getTextBytes().toByteArray()); |
| } |
| } |
| |
| private void writeXmlFrom(XmlElement element) throws IOException { |
| ANGLE_OPEN.writeTo(out); |
| if (!element.getNamespaceUriBytes().isEmpty()) { |
| findNamespacePrefix(element.getNamespaceUriBytes()).writeTo(out); |
| COLON.writeTo(out); |
| } |
| final ByteString name = element.getNameBytes(); |
| name.writeTo(out); |
| final Map<ByteString, ByteString> namespaces = new LinkedHashMap<>(); |
| for (XmlNamespace namespace : element.getNamespaceDeclarationList()) { |
| final ByteString prefix = namespace.getPrefixBytes(); |
| SPACE.writeTo(out); |
| XMLNS.writeTo(out); |
| prefix.writeTo(out); |
| EQUALS.writeTo(out); |
| quote(namespace.getUriBytes()); |
| namespaces.put(namespace.getUriBytes(), prefix); |
| } |
| namespaceStack.push(namespaces); |
| for (XmlAttribute attribute : element.getAttributeList()) { |
| SPACE.writeTo(out); |
| if (!attribute.getNamespaceUriBytes().isEmpty()) { |
| findNamespacePrefix(attribute.getNamespaceUriBytes()).writeTo(out); |
| COLON.writeTo(out); |
| } |
| attribute.getNameBytes().writeTo(out); |
| EQUALS.writeTo(out); |
| quote(attribute.getValueBytes()); |
| } |
| if (element.getChildList().isEmpty()) { |
| FORWARD_SLASH.writeTo(out); |
| ANGLE_CLOSE.writeTo(out); |
| } else { |
| ANGLE_CLOSE.writeTo(out); |
| for (XmlNode child : element.getChildList()) { |
| writeXmlFrom(child); |
| } |
| ANGLE_OPEN.writeTo(out); |
| FORWARD_SLASH.writeTo(out); |
| if (!element.getNamespaceUriBytes().isEmpty()) { |
| findNamespacePrefix(element.getNamespaceUriBytes()).writeTo(out); |
| COLON.writeTo(out); |
| } |
| name.writeTo(out); |
| ANGLE_CLOSE.writeTo(out); |
| } |
| namespaceStack.pop(); |
| } |
| |
| private void quote(ByteString bytes) throws IOException { |
| QUOTE.writeTo(out); |
| out.write( |
| XmlEscapers.xmlAttributeEscaper() |
| .escape(bytes.toStringUtf8()) |
| .getBytes(StandardCharsets.UTF_8)); |
| QUOTE.writeTo(out); |
| } |
| |
| private ByteString findNamespacePrefix(ByteString uri) { |
| for (Map<ByteString, ByteString> uriToPrefix : namespaceStack) { |
| if (uriToPrefix.containsKey(uri)) { |
| return uriToPrefix.get(uri); |
| } |
| } |
| throw new IllegalStateException( |
| "Unable to find prefix for " |
| + uri.toStringUtf8() |
| + " in [ " |
| + namespaceStack.stream() |
| .map(Map::keySet) |
| .flatMap(Set::stream) |
| .map(ByteString::toString) |
| .collect(joining(", ")) |
| + " ]"); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| out.close(); |
| } |
| } |
| |
| /** Traverses the resource table and compiled xml resource using the {@link ResourceVisitor}. */ |
| public <T extends ResourceVisitor> T visitResources(T visitor) throws IOException { |
| |
| // visit manifest |
| visitXmlResource(apkFileSystem.getPath(MANIFEST), visitor.enteringManifest()); |
| |
| // visit resource table and associated files. |
| final ResourceTable resourceTable = |
| ResourceTable.parseFrom( |
| Files.newInputStream(apkFileSystem.getPath(RESOURCE_TABLE)), |
| ExtensionRegistry.getEmptyRegistry()); |
| |
| final List<String> sourcePool = |
| resourceTable.hasSourcePool() |
| ? decodeSourcePool(resourceTable.getSourcePool().getData().toByteArray()) |
| : ImmutableList.of(); |
| |
| for (Package pkg : resourceTable.getPackageList()) { |
| ResourcePackageVisitor pkgVisitor = |
| visitor.enteringPackage(pkg.getPackageId().getId(), pkg.getPackageName()); |
| if (pkgVisitor != null) { |
| for (Resources.Type type : pkg.getTypeList()) { |
| ResourceTypeVisitor typeVisitor = |
| pkgVisitor.enteringResourceType( |
| type.getTypeId().getId(), ResourceType.getEnum(type.getName())); |
| if (typeVisitor != null) { |
| for (Entry entry : type.getEntryList()) { |
| ResourceValueVisitor entryVisitor = |
| typeVisitor.enteringDeclaration(entry.getName(), entry.getEntryId().getId()); |
| if (entryVisitor != null) { |
| for (ConfigValue configValue : entry.getConfigValueList()) { |
| if (configValue.hasValue()) { |
| visitValue(entryVisitor, configValue.getValue(), sourcePool); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| return visitor; |
| } |
| |
| /** Accessor for the underlying URI of the apk. */ |
| public URI asApk() { |
| return uri.normalize(); |
| } |
| |
| // TODO(72324748): Centralize duplicated code with AndroidCompiledDataDeserializer. |
| private static List<String> decodeSourcePool(byte[] bytes) throws UnsupportedEncodingException { |
| ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); |
| |
| int stringCount = byteBuffer.getInt(8); |
| boolean isUtf8 = (byteBuffer.getInt(16) & (1 << 8)) != 0; |
| int stringsStart = byteBuffer.getInt(20); |
| // Position the ByteBuffer after the metadata |
| byteBuffer.position(28); |
| |
| List<String> strings = new ArrayList<>(); |
| |
| for (int i = 0; i < stringCount; i++) { |
| int stringOffset = stringsStart + byteBuffer.getInt(); |
| |
| if (isUtf8) { |
| int characterCount = byteBuffer.get(stringOffset) & 0xFF; |
| if ((characterCount & 0x80) != 0) { |
| characterCount = |
| ((characterCount & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF); |
| } |
| |
| stringOffset += (characterCount >= 0x80 ? 2 : 1); |
| |
| int length = byteBuffer.get(stringOffset) & 0xFF; |
| if ((length & 0x80) != 0) { |
| length = ((length & 0x7F) << 8) | (byteBuffer.get(stringOffset + 1) & 0xFF); |
| } |
| |
| stringOffset += (length >= 0x80 ? 2 : 1); |
| |
| strings.add(new String(bytes, stringOffset, length, "UTF8")); |
| } else { |
| // TODO(b/148817379): this next block of lines is forming an int with holes in it. |
| int characterCount = byteBuffer.get(stringOffset) & 0xFFFF; |
| if ((characterCount & 0x8000) != 0) { |
| characterCount = |
| ((characterCount & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF); |
| } |
| |
| stringOffset += 2 * (characterCount >= 0x8000 ? 2 : 1); |
| |
| int length = byteBuffer.get(stringOffset) & 0xFFFF; |
| if ((length & 0x8000) != 0) { |
| length = ((length & 0x7FFF) << 16) | (byteBuffer.get(stringOffset + 2) & 0xFFFF); |
| } |
| |
| stringOffset += 2 * (length >= 0x8000 ? 2 : 1); |
| |
| strings.add(new String(bytes, stringOffset, length, "UTF16")); |
| } |
| } |
| |
| return strings; |
| } |
| |
| private void visitValue(ResourceValueVisitor entryVisitor, Value value, List<String> sourcePool) { |
| if (value.hasSource()) { |
| entryVisitor.entering(apkFileSystem.getPath(sourcePool.get(value.getSource().getPathIdx()))); |
| } |
| switch (value.getValueCase()) { |
| case ITEM: |
| visitItem(entryVisitor, value.getItem()); |
| break; |
| case COMPOUND_VALUE: |
| visitCompoundValue(entryVisitor, value.getCompoundValue()); |
| break; |
| default: |
| throw new IllegalStateException( |
| "Config value does not have a declared value case: " + value); |
| } |
| } |
| |
| private void visitCompoundValue(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| switch (value.getValueCase()) { |
| case STYLE: |
| visitStyle(entryVisitor, value); |
| break; |
| case STYLEABLE: |
| visitStyleable(entryVisitor, value); |
| break; |
| case ATTR: |
| visitAttr(entryVisitor, value); |
| break; |
| case ARRAY: |
| visitArray(entryVisitor, value); |
| break; |
| case PLURAL: |
| visitPlural(entryVisitor, value); |
| break; |
| default: |
| } |
| } |
| |
| private void visitPlural(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| value.getPlural().getEntryList().stream() |
| .filter(Plural.Entry::hasItem) |
| .map(Plural.Entry::getItem) |
| .forEach( |
| i -> { |
| switch (i.getValueCase()) { |
| case FILE: |
| visitFile(entryVisitor, i.getFile()); |
| break; |
| case REF: |
| visitReference(entryVisitor, i.getRef()); |
| break; |
| default: |
| } |
| }); |
| } |
| |
| private void visitArray(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| value.getArray().getElementList().stream() |
| .filter(Array.Element::hasItem) |
| .map(Array.Element::getItem) |
| .forEach( |
| i -> { |
| switch (i.getValueCase()) { |
| case FILE: |
| visitFile(entryVisitor, i.getFile()); |
| break; |
| case REF: |
| visitReference(entryVisitor, i.getRef()); |
| break; |
| default: |
| } |
| }); |
| } |
| |
| private void visitAttr(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| value.getAttr().getSymbolList().stream() |
| .filter(Symbol::hasName) |
| .map(Symbol::getName) |
| .forEach(name -> visitReference(entryVisitor, name)); |
| } |
| |
| private void visitStyleable(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| value.getStyleable().getEntryList().forEach(e -> visitReference(entryVisitor, e.getAttr())); |
| } |
| |
| private void visitStyle(ResourceValueVisitor entryVisitor, CompoundValue value) { |
| final Style style = value.getStyle(); |
| if (style.hasParent()) { |
| visitReference(entryVisitor, style.getParent()); |
| } |
| for (Style.Entry entry : style.getEntryList()) { |
| if (entry.hasItem()) { |
| visitItem(entryVisitor, entry.getItem()); |
| } |
| if (entry.hasKey()) { |
| visitReference(entryVisitor, entry.getKey()); |
| } |
| } |
| } |
| |
| private void visitItem(ResourceValueVisitor entryVisitor, Item item) { |
| switch (item.getValueCase()) { |
| case FILE: |
| visitFile(entryVisitor, item.getFile()); |
| break; |
| case REF: |
| visitReference(entryVisitor, item.getRef()); |
| break; |
| default: |
| } |
| } |
| |
| private void visitFile(ResourceValueVisitor entryVisitor, FileReference file) { |
| final Path path = apkFileSystem.getPath(file.getPath()); |
| if (file.getType() == FileReference.Type.PROTO_XML) { |
| visitXmlResource(path, entryVisitor.entering(path)); |
| } else if (file.getType() != FileReference.Type.PNG) { |
| entryVisitor.acceptOpaqueFileType(path); |
| } |
| } |
| |
| private void visitXmlResource(Path path, ReferenceVisitor visitor) { |
| if (visitor == null) { |
| return; |
| } |
| |
| try (InputStream in = Files.newInputStream(path)) { |
| visit(XmlNode.parseFrom(in, ExtensionRegistry.getEmptyRegistry()), visitor); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void visit(XmlNode node, ReferenceVisitor visitor) { |
| if (node.hasElement()) { |
| final XmlElement element = node.getElement(); |
| Consumer<XmlNode> visit = |
| "wearableApp".equals(element.getName()) |
| ? n -> visitWearableApp(n, visitor) |
| : n -> visit(n, visitor); |
| |
| visitAttributes(visitor, element); |
| |
| element.getChildList().forEach(visit); |
| } |
| } |
| |
| private void visitAttributes(ReferenceVisitor visitor, XmlElement element) { |
| for (XmlAttribute attribute : element.getAttributeList()) { |
| if (attribute.getCompiledItem().hasRef()) { |
| visitReference(visitor, attribute.getCompiledItem().getRef()); |
| } |
| if (attribute.getResourceId() != 0) { |
| visitor.accept(attribute.getResourceId()); |
| } |
| } |
| } |
| |
| // TODO(b/113166518): Remove when wearable apps have real references. |
| // this doesn't belong in the protoapk, but has to be included for the time being. |
| private void visitWearableApp(XmlNode node, ReferenceVisitor visitor) { |
| if (node.hasElement()) { |
| final XmlElement element = node.getElement(); |
| if ("rawPathResId".equals(element.getName())) { |
| visitor.accept( |
| ResourceType.RAW.getName() |
| + "/" |
| + element.getChildList().stream().map(XmlNode::getText).collect(joining())); |
| } else { |
| visitAttributes(visitor, element); |
| } |
| element.getChildList().forEach(c -> visitWearableApp(c, visitor)); |
| } |
| } |
| |
| private void visitReference(ReferenceVisitor visitor, Reference ref) { |
| if (ref.getId() != 0) { |
| logger.finest( |
| "Visiting ref by id " + ref.getName() + "=" + "0x" + Integer.toHexString(ref.getId())); |
| visitor.accept(ref.getId()); |
| } else if (!ref.getName().isEmpty()) { |
| logger.finest("Visiting ref by name " + ref); |
| visitor.accept(ref.getName()); |
| } else { |
| logger.finest("Visiting null by name " + ref); |
| visitor.acceptNullReference(); |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| apkFileSystem.close(); |
| } |
| |
| /** Provides an entry point to recording declared and referenced resources in the apk. */ |
| public interface ResourceVisitor { |
| /** Called when entering the manifest. If null, the manifest is not visited. */ |
| @Nullable |
| ManifestVisitor enteringManifest(); |
| |
| /** Called when entering a resource package. If null, the package is not visited. */ |
| @Nullable |
| ResourcePackageVisitor enteringPackage(int pkgId, String packageName); |
| } |
| |
| /** Provides a visitor for packages. */ |
| public interface ResourcePackageVisitor { |
| /** Called when entering the resource types of the package. If null, the type is not visited. */ |
| @Nullable |
| ResourceTypeVisitor enteringResourceType(int typeId, ResourceType type); |
| } |
| |
| /** Visitor for resources types */ |
| public interface ResourceTypeVisitor { |
| /** |
| * Called for resource declarations. |
| * |
| * @param name The name of the resource. |
| * @param resourceId The id of the resource, without the package and type. |
| * @return A visitor for accepting references to other resources from the declared resource. If |
| * null, the value is not visited. |
| */ |
| @Nullable |
| ResourceValueVisitor enteringDeclaration(String name, int resourceId); |
| } |
| |
| /** A manifest specific resource reference visitor. */ |
| public interface ManifestVisitor extends ReferenceVisitor {} |
| |
| /** General resource reference visitor. */ |
| public interface ResourceValueVisitor extends ReferenceVisitor { |
| /** Called when entering the source of a value. Maybe called multiple times for each value. */ |
| ReferenceVisitor entering(Path path); |
| |
| /* Called when a raw resource contains a non-proto xml file type. */ |
| void acceptOpaqueFileType(Path path); |
| } |
| |
| /** Role interface for visiting resource references. */ |
| public interface ReferenceVisitor { |
| /** Called when a reference is defined by name (resourceType/name). */ |
| void accept(String name); |
| |
| /** Called when a reference is defined by id (full id, with package and type.) */ |
| void accept(int value); |
| |
| /** Called when a reference is null. */ |
| default void acceptNullReference() { |
| // pass |
| } |
| } |
| } |