Java tutorial
// 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 java.util.stream.Collectors.joining; import com.android.build.gradle.tasks.ResourceUsageAnalyzer; import com.android.resources.ResourceType; import com.android.tools.lint.checks.ResourceUsageModel; import com.android.tools.lint.checks.ResourceUsageModel.Resource; import com.android.tools.lint.detector.api.LintUtils; import com.android.utils.XmlUtils; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.android.aapt2.ProtoApk.ManifestVisitor; import com.google.devtools.build.android.aapt2.ProtoApk.ReferenceVisitor; import com.google.devtools.build.android.aapt2.ProtoApk.ResourcePackageVisitor; import com.google.devtools.build.android.aapt2.ProtoApk.ResourceValueVisitor; import com.google.devtools.build.android.aapt2.ProtoApk.ResourceVisitor; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Logger; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Attr; import org.w3c.dom.DOMException; import org.w3c.dom.NamedNodeMap; import org.xml.sax.SAXException; /** A resource usage analyzer tha functions on apks in protocol buffer format. */ public class ProtoResourceUsageAnalyzer extends ResourceUsageAnalyzer { public ProtoResourceUsageAnalyzer(Set<String> resourcePackages, Path mapping, Path logFile) throws DOMException, ParserConfigurationException { super(resourcePackages, null, null, null, mapping, null, logFile); } private static Resource parse(ResourceUsageModel model, String resourceTypeAndName) { final Iterator<String> iterator = Splitter.on('/').split(resourceTypeAndName).iterator(); Preconditions.checkArgument(iterator.hasNext(), "%s invalid resource name", resourceTypeAndName); ResourceType resourceType = ResourceType.getEnum(iterator.next()); Preconditions.checkArgument(iterator.hasNext(), "%s invalid resource name", resourceTypeAndName); return model.getResource(resourceType, iterator.next()); } /** * Calculate and removes unused resource from the {@link ProtoApk}. * * @param apk An apk in the aapt2 proto format. * @param classes The associated classes for the apk. * @param destination Where to write the reduced resources. * @param keep A list of resource urls to keep, unused or not. * @param discard A list of resource urls to always discard. */ public void shrink(ProtoApk apk, Path classes, Path destination, Collection<String> keep, Collection<String> discard) throws IOException, ParserConfigurationException, SAXException { // record resources and manifest apk.visitResources( // First, collect all declarations using the declaration visitor. // This allows the model to start with a defined set of resources to build the reference // graph on. apk.visitResources(new ResourceDeclarationVisitor(model())).toUsageVisitor()); recordClassUsages(classes); // Have to give the model xml attributes with keep and discard urls. final NamedNodeMap toolAttributes = XmlUtils.parseDocument(String.format( "<resources xmlns:tools='http://schemas.android.com/tools' tools:keep='%s'" + " tools:discard='%s'></resources>", keep.stream().collect(joining(",")), discard.stream().collect(joining(","))), true) .getDocumentElement().getAttributes(); for (int i = 0; i < toolAttributes.getLength(); i++) { model().recordToolsAttributes((Attr) toolAttributes.item(i)); } model().processToolsAttributes(); keepPossiblyReferencedResources(); dumpReferences(); // Remove unused. final ImmutableSet<Resource> unused = ImmutableSet.copyOf(model().findUnused()); // ResourceUsageAnalyzer uses the logger to generate the report. Logger logger = Logger.getLogger(getClass().getName()); unused.forEach(resource -> logger .fine("Deleted unused file " + ((resource.locations != null && resource.locations.getFile() != null) ? resource.locations.getFile().toString() : "<apk>" + " for resource " + resource))); apk.copy(destination, (resourceType, name) -> !unused .contains(Preconditions.checkNotNull(model().getResource(resourceType, name), "%s/%s was not declared but is copied!", resourceType, name))); } private static final class ResourceDeclarationVisitor implements ResourceVisitor { private final ResourceShrinkerUsageModel model; private final Set<Integer> packageIds = new HashSet<>(); public ResourceDeclarationVisitor(ResourceShrinkerUsageModel model) { this.model = model; } @javax.annotation.Nullable @Override public ManifestVisitor enteringManifest() { return null; } @Override public ResourcePackageVisitor enteringPackage(int pkgId, String packageName) { packageIds.add(pkgId); return (typeId, resourceType) -> (name, resourceId) -> { String hexId = String.format("0x%s", Integer.toHexString(((pkgId << 24) | (typeId << 16) | resourceId))); model.addDeclaredResource(resourceType, LintUtils.getFieldName(name), hexId, true); // Skip visiting the definition when collecting declarations. return null; }; } ResourceUsageVisitor toUsageVisitor() { return new ResourceUsageVisitor(model, ImmutableSet.copyOf(packageIds)); } } private static final class ResourceUsageVisitor implements ResourceVisitor { private final ResourceShrinkerUsageModel model; private final ImmutableSet<Integer> packageIds; private ResourceUsageVisitor(ResourceShrinkerUsageModel model, ImmutableSet<Integer> packageIds) { this.model = model; this.packageIds = packageIds; } @Override public ManifestVisitor enteringManifest() { return new ManifestVisitor() { @Override public void accept(String name) { ResourceUsageModel.markReachable(model.getResourceFromUrl(name)); } @Override public void accept(int value) { ResourceUsageModel.markReachable(model.getResource(value)); } }; } @Override public ResourcePackageVisitor enteringPackage(int pkgId, String packageName) { return (typeId, resourceType) -> (name, resourceId) -> new ResourceUsageValueVisitor(model, model.getResource(resourceType, name), packageIds); } } private static final class ResourceUsageValueVisitor implements ResourceValueVisitor { private final ResourceUsageModel model; private final Resource declaredResource; private final ImmutableSet<Integer> packageIds; private ResourceUsageValueVisitor(ResourceUsageModel model, Resource declaredResource, ImmutableSet<Integer> packageIds) { this.model = model; this.declaredResource = declaredResource; this.packageIds = packageIds; } @Override public ReferenceVisitor entering(Path path) { return this; } @Override public void acceptOpaqueFileType(Path path) { try { String pathString = path.toString(); if (pathString.endsWith(".js")) { model.tokenizeJs(declaredResource, new String(java.nio.file.Files.readAllBytes(path), StandardCharsets.UTF_8)); } else if (pathString.endsWith(".css")) { model.tokenizeCss(declaredResource, new String(java.nio.file.Files.readAllBytes(path), StandardCharsets.UTF_8)); } else if (pathString.endsWith(".html")) { model.tokenizeHtml(declaredResource, new String(java.nio.file.Files.readAllBytes(path), StandardCharsets.UTF_8)); } else { // Path is a reference to the apk zip -- unpack it before getting a file reference. model.tokenizeUnknownBinary(declaredResource, java.nio.file.Files .copy(path, java.nio.file.Files.createTempFile("binary-resource", null), StandardCopyOption.REPLACE_EXISTING) .toFile()); } } catch (IOException e) { throw new RuntimeException(e); } } @Override public void accept(String name) { parse(model, name).addReference(declaredResource); } @Override public void accept(int value) { if (isInDeclaredPackages(value)) { // ignore references outside of scanned packages. declaredResource.addReference(model.getResource(value)); } } /** Tests if the id is in any of the scanned packages. */ private boolean isInDeclaredPackages(int value) { return packageIds.contains(value >> 24); } } }