From 88c30e0b8d01ae3afd932309ca368d8426cf3d59 Mon Sep 17 00:00:00 2001 From: Andrew Pellegrini Date: Tue, 20 Dec 2016 18:50:59 +0000 Subject: Delete Bazel local version ResourceShrinker.java. -- PiperOrigin-RevId: 142573704 MOS_MIGRATED_REVID=142573704 --- .../devtools/build/android/ResourceShrinker.java | 1177 -------------------- 1 file changed, 1177 deletions(-) delete mode 100644 src/tools/android/java/com/google/devtools/build/android/ResourceShrinker.java (limited to 'src/tools/android/java/com/google/devtools') diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinker.java b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinker.java deleted file mode 100644 index e5dbad9646..0000000000 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinker.java +++ /dev/null @@ -1,1177 +0,0 @@ -// Copyright 2015 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; - -import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PARENT; -import static com.android.SdkConstants.ATTR_TYPE; -import static com.android.SdkConstants.DOT_CLASS; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.FD_RES_VALUES; -import static com.android.SdkConstants.PREFIX_ANDROID; -import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_RESOURCES; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.utils.SdkUtils.endsWithIgnoreCase; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.ide.common.resources.ResourceUrl; -import com.android.ide.common.resources.configuration.DensityQualifier; -import com.android.ide.common.resources.configuration.FolderConfiguration; -import com.android.ide.common.resources.configuration.ResourceQualifier; -import com.android.ide.common.xml.XmlPrettyPrinter; -import com.android.resources.FolderTypeRelationship; -import com.android.resources.ResourceFolderType; -import com.android.resources.ResourceType; -import com.android.utils.Pair; -import com.android.utils.XmlUtils; -import com.google.common.base.Joiner; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.io.ByteStreams; -import com.google.common.io.Closeables; -import com.google.common.io.Files; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.FileHandler; -import java.util.logging.Formatter; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.logging.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import javax.xml.parsers.ParserConfigurationException; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -/** - * Class responsible for searching through a Gradle built tree (after resource merging, compilation - * and ProGuarding has been completed, but before final .apk assembly), which figures out which - * resources if any are unused, and removes them. - *

It does this by examining - *

- * From all this, it builds up a reference graph, and based on the root references (e.g. from the - * manifest and from the remaining code) it computes which resources are actually reachable in the - * app, and anything that is not reachable is then marked for deletion. - *

A resource is referenced in code if either the field R.type.name is referenced (which is the - * case for non-final resource references, e.g. in libraries), or if the corresponding int value is - * referenced (for final resource values). We check this by looking at the ProGuard output classes - * with an ASM visitor. One complication is that code can also call - * {@code Resources#getIdentifier(String,String,String)} where they can pass in the names of - * resources to look up. To handle this scenario, we use the ClassVisitor to see if there are any - * calls to the specific {@code Resources#getIdentifier} method. If not, great, the usage analysis - * is completely accurate. If we do find one, we check all the string constants found - * anywhere in the app, and look to see if any look relevant. For example, if we find the string - * "string/foo" or "my.pkg:string/foo", we will then mark the string resource named foo (if any) as - * potentially used. Similarly, if we find just "foo" or "/foo", we will mark all resources - * named "foo" as potentially used. However, if the string is "bar/foo" or " foo " these strings are - * ignored. This means we can potentially miss resources usages where the resource name is completed - * computed (e.g. by concatenating individual characters or taking substrings of strings that do not - * look like resource names), but that seems extremely unlikely to be a real-world scenario.

For - * now, for reasons detailed in the code, this only applies to file-based resources like layouts, - * menus and drawables, not value-based resources like strings and dimensions. - */ -public class ResourceShrinker { - - public static final int TYPICAL_RESOURCE_COUNT = 200; - private final Set resourcePackages; - private final Path rTxt; - private final Path proguardMapping; - private final Path classesJar; - private final Path mergedManifest; - private final Path mergedResourceDir; - private final Logger logger; - - /** - * The computed set of unused resources - */ - private List unused; - /** - * List of all known resources (parsed from R.java) - */ - private List resources = Lists.newArrayListWithExpectedSize(TYPICAL_RESOURCE_COUNT); - /** - * Map from R field value to corresponding resource - */ - private Map valueToResource = - Maps.newHashMapWithExpectedSize(TYPICAL_RESOURCE_COUNT); - /** - * Map from resource type to map from resource name to resource object - */ - private Map> typeToName = - Maps.newEnumMap(ResourceType.class); - /** - * Map from resource class owners (VM format class) to corresponding resource entries. - * This lets us map back from code references (obfuscated class and possibly obfuscated field - * reference) back to the corresponding resource type and name. - */ - private final Map>> resourceObfuscation = - Maps.newHashMapWithExpectedSize(30); - - public ResourceShrinker( - Set resourcePackages, - @NonNull Path rTxt, - @NonNull Path classesJar, - @NonNull Path manifest, - @Nullable Path mapping, - @NonNull Path resources, - Path logFile) { - this.resourcePackages = resourcePackages; - this.rTxt = rTxt; - this.proguardMapping = mapping; - this.classesJar = classesJar; - this.mergedManifest = manifest; - this.mergedResourceDir = resources; - - this.logger = Logger.getLogger(getClass().getName()); - logger.setLevel(Level.FINE); - if (logFile != null) { - try { - FileHandler fileHandler = new FileHandler(logFile.toString()); - fileHandler.setLevel(Level.FINE); - fileHandler.setFormatter(new Formatter(){ - @Override public String format(LogRecord record) { - return record.getMessage() + "\n"; - } - }); - logger.addHandler(fileHandler); - } catch (SecurityException | IOException e) { - logger.warning(String.format("Unable to open '%s' to write log.", logFile)); - } - } - } - - public void shrink(Path destinationDir) throws IOException, - ParserConfigurationException, SAXException { - parseResourceTxtFile(rTxt, resourcePackages); - recordMapping(proguardMapping); - recordUsages(classesJar); - recordManifestUsages(mergedManifest); - recordResources(mergedResourceDir); - keepPossiblyReferencedResources(); - dumpReferences(); - findUnused(); - removeUnused(destinationDir); - } - - /** - * Remove resources (already identified by {@link #shrink(Path)}). - * - *

This task will copy all remaining used resources over from the full resource directory to a - * new reduced resource directory and removes unused values from all value xml files. - * - * @param destination directory to copy resources into; if null, delete resources in place - */ - private void removeUnused(Path destination) throws IOException, - ParserConfigurationException, SAXException { - assert unused != null; // should always call analyze() first - int resourceCount = unused.size() * 4; // *4: account for some resource folder repetition - Set skip = Sets.newHashSetWithExpectedSize(resourceCount); - Set rewrite = Sets.newHashSetWithExpectedSize(resourceCount); - Set deleted = Sets.newHashSetWithExpectedSize(resourceCount); - for (Resource resource : unused) { - deleted.add(resource); - if (resource.declarations != null) { - for (File file : resource.declarations) { - String folder = file.getParentFile().getName(); - ResourceFolderType folderType = ResourceFolderType.getFolderType(folder); - if (folderType != null && folderType != ResourceFolderType.VALUES) { - logger.fine("Deleted unused resource " + file); - assert skip != null; - skip.add(file); - } else { - // Can't delete values immediately; there can be many resources - // in this file, so we have to process them all - rewrite.add(file); - } - } - } - } - // Special case the base values.xml folder - File values = new File(mergedResourceDir.toFile(), - FD_RES_VALUES + File.separatorChar + "values.xml"); - if (values.exists()) { - rewrite.add(values); - } - - Map rewritten = Maps.newHashMapWithExpectedSize(rewrite.size()); - rewriteXml(rewrite, rewritten); - // TODO(apell): The graph traversal does not mark IDs as reachable or not, so they cannot be - // accurately removed from public.xml, but the declarations may be deleted if they occur in - // other files. IDs should be added to values.xml so that there are no definitions in public.xml - // without declarations. - createStubIds(values, rewritten); - - File publicXml = new File(mergedResourceDir.toFile(), - FD_RES_VALUES + File.separatorChar + "public.xml"); - trimPublicResources(publicXml, deleted, rewritten); - - filteredCopy(mergedResourceDir.toFile(), destination, skip, rewritten); - } - - /** - * Deletes unused resources from value XML files. - */ - private void rewriteXml(Set rewrite, Map rewritten) - throws IOException, ParserConfigurationException, SAXException { - // Delete value resources: Must rewrite the XML files - for (File file : rewrite) { - String xml = Files.toString(file, UTF_8); - Document document = XmlUtils.parseDocument(xml, true); - Element root = document.getDocumentElement(); - if (root != null && TAG_RESOURCES.equals(root.getTagName())) { - List removed = Lists.newArrayList(); - stripUnused(root, removed); - logger.fine("Removed " + removed.size() + " unused resources from " + file + ":\n " - + Joiner.on(", ").join(removed)); - String formatted = XmlPrettyPrinter.prettyPrint(document, xml.endsWith("\n")); - rewritten.put(file, formatted); - } - } - } - - /** - * Write stub values for IDs to values.xml to match those available in public.xml. - */ - private void createStubIds(File values, Map rewritten) - throws IOException, ParserConfigurationException, SAXException { - if (values.exists()) { - String xml = rewritten.get(values); - if (xml == null) { - xml = Files.toString(values, UTF_8); - } - List stubbed = Lists.newArrayList(); - Document document = XmlUtils.parseDocument(xml, true); - Element root = document.getDocumentElement(); - for (Resource resource : resources) { - if (resource.type == ResourceType.ID && !resource.hasDefault) { - Element item = document.createElement(TAG_ITEM); - item.setAttribute(ATTR_TYPE, resource.type.getName()); - item.setAttribute(ATTR_NAME, resource.name); - root.appendChild(item); - stubbed.add(resource.getUrl()); - } - } - logger.fine("Created " + stubbed.size() + " stub IDs for:\n " - + Joiner.on(", ").join(stubbed)); - String formatted = XmlPrettyPrinter.prettyPrint(document, xml.endsWith("\n")); - rewritten.put(values, formatted); - } - } - - /** - * Remove public definitions of unused resources. - */ - private void trimPublicResources(File publicXml, Set deleted, - Map rewritten) throws IOException, ParserConfigurationException, SAXException { - if (publicXml.exists()) { - String xml = rewritten.get(publicXml); - if (xml == null) { - xml = Files.toString(publicXml, UTF_8); - } - Document document = XmlUtils.parseDocument(xml, true); - Element root = document.getDocumentElement(); - if (root != null && TAG_RESOURCES.equals(root.getTagName())) { - NodeList children = root.getChildNodes(); - for (int i = children.getLength() - 1; i >= 0; i--) { - Node child = children.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - Element resourceElement = (Element) child; - ResourceType type = ResourceType.getEnum(resourceElement.getAttribute(ATTR_TYPE)); - String name = resourceElement.getAttribute(ATTR_NAME); - if (type != null && name != null) { - Resource resource = getResource(type, name); - if (resource != null && deleted.contains(resource)) { - root.removeChild(child); - } - } - } - } - } - String formatted = XmlPrettyPrinter.prettyPrint(document, xml.endsWith("\n")); - rewritten.put(publicXml, formatted); - } - } - - /** - * Copies one resource directory tree into another; skipping some files, replacing the contents of - * some, and passing everything else through unmodified - */ - private static void filteredCopy(File source, Path destination, Set skip, - Map replace) throws IOException { - - File destinationFile = destination.toFile(); - if (source.isDirectory()) { - File[] children = source.listFiles(); - if (children != null) { - if (!destinationFile.exists()) { - boolean success = destinationFile.mkdirs(); - if (!success) { - throw new IOException("Could not create " + destination); - } - } - for (File child : children) { - filteredCopy(child, destination.resolve(child.getName()), skip, replace); - } - } - } else if (!skip.contains(source) && source.isFile()) { - String contents = replace.get(source); - if (contents != null) { - Files.write(contents, destinationFile, UTF_8); - } else { - Files.copy(source, destinationFile); - } - } - } - - private void stripUnused(Element element, List removed) { - ResourceType type = getResourceType(element); - if (type == ResourceType.ATTR) { - // Not yet properly handled - return; - } - Resource resource = getResource(element); - if (resource != null) { - if (resource.type == ResourceType.DECLARE_STYLEABLE - || resource.type == ResourceType.ATTR) { - // Don't strip children of declare-styleable; we're not correctly - // tracking field references of the R_styleable_attr fields yet - return; - } - if (!resource.reachable - && (resource.type == ResourceType.STYLE - || resource.type == ResourceType.PLURALS - || resource.type == ResourceType.ARRAY)) { - NodeList children = element.getChildNodes(); - for (int i = children.getLength() - 1; i >= 0; i--) { - Node child = children.item(i); - element.removeChild(child); - } - } - } - NodeList children = element.getChildNodes(); - for (int i = children.getLength() - 1; i >= 0; i--) { - Node child = children.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - stripUnused((Element) child, removed); - } - } - if (resource != null && !resource.reachable && resource.isRelevantType()) { - removed.add(resource.getUrl()); - Node parent = element.getParentNode(); - parent.removeChild(element); - } - } - - private static String getFieldName(Element element) { - return getFieldName(element.getAttribute(ATTR_NAME)); - } - - @Nullable - private Resource getResource(Element element) { - ResourceType type = getResourceType(element); - if (type != null) { - String name = getFieldName(element); - return getResource(type, name); - } - return null; - } - - private static ResourceType getResourceType(Element element) { - String tagName = element.getTagName(); - switch (tagName) { - case TAG_ITEM: - String typeName = element.getAttribute(ATTR_TYPE); - if (!typeName.isEmpty()) { - return ResourceType.getEnum(typeName); - } - break; - case "string-array": - case "integer-array": - return ResourceType.ARRAY; - default: - return ResourceType.getEnum(tagName); - } - return null; - } - - private void findUnused() { - List roots = Lists.newArrayList(); - for (Resource resource : resources) { - if (resource.reachable && resource.type != ResourceType.ID - && resource.type != ResourceType.ATTR) { - roots.add(resource); - } - } - logger.fine(String.format("The root reachable resources are:\n %s", - Joiner.on(",\n ").join(roots))); - Map seen = new IdentityHashMap<>(resources.size()); - for (Resource root : roots) { - visit(root, seen); - } - List unused = Lists.newArrayListWithExpectedSize(resources.size()); - for (Resource resource : resources) { - if (!resource.reachable && resource.isRelevantType()) { - unused.add(resource); - } - } - this.unused = unused; - } - - private static void visit(Resource root, Map seen) { - if (seen.containsKey(root)) { - return; - } - seen.put(root, Boolean.TRUE); - root.reachable = true; - if (root.references != null) { - for (Resource referenced : root.references) { - visit(referenced, seen); - } - } - } - - private void dumpReferences() { - for (Resource resource : resources) { - if (resource.references != null) { - logger.fine(resource + " => " + resource.references); - } - } - } - - private void keepPossiblyReferencedResources() { - if (!mFoundGetIdentifier || mStrings == null) { - // No calls to android.content.res.Resources#getIdentifier; no need - // to worry about string references to resources - return; - } - List strings = new ArrayList(mStrings); - Collections.sort(strings); - logger.fine(String.format("android.content.res.Resources#getIdentifier present: %s", - mFoundGetIdentifier)); - logger.fine("Referenced Strings:"); - for (String s : strings) { - s = s.trim().replace("\n", "\\n"); - if (s.length() > 40) { - s = s.substring(0, 37) + "..."; - } else if (s.isEmpty()) { - continue; - } - logger.fine(" " + s); - } - - Set names = Sets.newHashSetWithExpectedSize(50); - for (Map map : typeToName.values()) { - names.addAll(map.keySet()); - } - for (String string : mStrings) { - // Check whether the string looks relevant - // We consider three types of strings: - // (1) simple resource names, e.g. "foo" from @layout/foo - // These might be the parameter to a getIdentifier() call, or could - // be composed into a fully qualified resource name for the getIdentifier() - // method. We match these for *all* resource types. - // (2) Relative source names, e.g. layout/foo, from @layout/foo - // These might be composed into a fully qualified resource name for - // getIdentifier(). - // (3) Fully qualified resource names of the form package:type/name. - int n = string.length(); - boolean justName = true; - boolean haveSlash = false; - for (int i = 0; i < n; i++) { - char c = string.charAt(i); - if (c == '/') { - haveSlash = true; - justName = false; - } else if (c == '.' || c == ':') { - justName = false; - } else if (!Character.isJavaIdentifierPart(c)) { - // This shouldn't happen; we've filtered out these strings in - // the {@link #referencedString} method - assert false : string; - break; - } - } - String name; - if (justName) { - // Check name (below) - name = string; - } else if (!haveSlash) { - // If we have more than just a symbol name, we expect to also see a slash - //noinspection UnnecessaryContinue - continue; - } else { - // Try to pick out the resource name pieces; if we can find the - // resource type unambiguously; if not, just match on names - int slash = string.indexOf('/'); - assert slash != -1; // checked with haveSlash above - name = string.substring(slash + 1); - if (name.isEmpty() || !names.contains(name)) { - continue; - } - // See if have a known specific resource type - if (slash > 0) { - int colon = string.indexOf(':'); - String typeName = string.substring(colon != -1 ? colon + 1 : 0, slash); - ResourceType type = ResourceType.getEnum(typeName); - if (type == null) { - continue; - } - Resource resource = getResource(type, name); - if (resource != null) { - logger.fine("Marking " + resource + " used because it " - + "matches string pool constant " + string); - } - markReachable(resource); - continue; - } - // fall through and check the name - } - if (names.contains(name)) { - for (Map map : typeToName.values()) { - Resource resource = map.get(string); - if (resource != null) { - logger.fine("Marking " + resource + " used because it " - + "matches string pool constant " + string); - } - markReachable(resource); - } - } else if (Character.isDigit(name.charAt(0))) { - // Just a number? There are cases where it calls getIdentifier by - // a String number; see for example SuggestionsAdapter in the support - // library which reports supporting a string like "2130837524" and - // "android.resource://com.android.alarmclock/2130837524". - try { - int id = Integer.parseInt(name); - if (id != 0) { - markReachable(valueToResource.get(id)); - } - } catch (NumberFormatException e) { - // pass - } - } - } - } - - private void recordResources(Path resDir) - throws IOException, SAXException, ParserConfigurationException { - - File[] resourceFolders = resDir.toFile().listFiles(); - if (resourceFolders != null) { - for (File folder : resourceFolders) { - ResourceFolderType folderType = ResourceFolderType.getFolderType(folder.getName()); - if (folderType != null) { - recordResources(folderType, folder); - } - } - } - } - - private void recordResources(@NonNull ResourceFolderType folderType, File folder) - throws ParserConfigurationException, SAXException, IOException { - File[] files = folder.listFiles(); - FolderConfiguration config = FolderConfiguration.getConfigForFolder(folder.getName()); - boolean isDefaultFolder = false; - if (config != null) { - isDefaultFolder = true; - for (int i = 0, n = FolderConfiguration.getQualifierCount(); i < n; i++) { - ResourceQualifier qualifier = config.getQualifier(i); - // Densities are special: even if they're present in just (say) drawable-hdpi - // we'll match it on any other density - if (qualifier != null && !(qualifier instanceof DensityQualifier)) { - isDefaultFolder = false; - break; - } - } - } - if (files != null) { - for (File file : files) { - String path = file.getPath(); - boolean isXml = endsWithIgnoreCase(path, DOT_XML); - Resource from = null; - // Record resource for the whole file - if (folderType != ResourceFolderType.VALUES) { - List types = FolderTypeRelationship.getRelatedResourceTypes( - folderType); - ResourceType type = types.get(0); - assert type != ResourceType.ID : folderType; - String name = file.getName(); - int extension = name.indexOf('.'); - if (extension > 0) { - name = name.substring(0, extension); - } - Resource resource = getResource(type, name); - if (resource != null) { - resource.addLocation(file); - if (isDefaultFolder) { - resource.hasDefault = true; - } - from = resource; - } - } - if (isXml) { - // For value files, and drawables and colors etc also pull in resource - // references inside the file - recordResourcesUsages(file, isDefaultFolder, from); - } - } - } - } - - private void recordMapping(@Nullable Path mapping) throws IOException { - if (mapping == null || !mapping.toFile().exists()) { - return; - } - final String arrowIndicator = " -> "; - final String resourceIndicator = ".R$"; - Map nameMap = null; - for (String line : Files.readLines(mapping.toFile(), UTF_8)) { - if (line.startsWith(" ") || line.startsWith("\t")) { - if (nameMap != null) { - // We're processing the members of a resource class: record names into the map - int n = line.length(); - int i = 0; - for (; i < n; i++) { - if (!Character.isWhitespace(line.charAt(i))) { - break; - } - } - if (i < n && line.startsWith("int", i)) { // int or int[] - int start = line.indexOf(' ', i + 3) + 1; - int arrow = line.indexOf(arrowIndicator); - if (start > 0 && arrow != -1) { - int end = line.indexOf(' ', start + 1); - if (end != -1) { - String oldName = line.substring(start, end); - String newName = line.substring(arrow + arrowIndicator.length()).trim(); - if (!newName.equals(oldName)) { - nameMap.put(newName, oldName); - } - } - } - } - } - continue; - } else { - nameMap = null; - } - int index = line.indexOf(resourceIndicator); - if (index == -1) { - continue; - } - int arrow = line.indexOf(arrowIndicator, index + 3); - if (arrow == -1) { - continue; - } - String typeName = line.substring(index + resourceIndicator.length(), arrow); - ResourceType type = ResourceType.getEnum(typeName); - if (type == null) { - continue; - } - int end = line.indexOf(':', arrow + arrowIndicator.length()); - if (end == -1) { - end = line.length(); - } - String target = line.substring(arrow + arrowIndicator.length(), end).trim(); - String ownerName = target.replace('.', '/'); - - nameMap = Maps.newHashMap(); - Pair> pair = Pair.of(type, nameMap); - resourceObfuscation.put(ownerName, pair); - } - } - - private void recordManifestUsages(Path manifest) - throws IOException, ParserConfigurationException, SAXException { - String xml = Files.toString(manifest.toFile(), UTF_8); - Document document = XmlUtils.parseDocument(xml, true); - recordManifestUsages(document.getDocumentElement()); - } - - private void recordResourcesUsages(@NonNull File file, boolean isDefaultFolder, - @Nullable Resource from) - throws IOException, ParserConfigurationException, SAXException { - String xml = Files.toString(file, UTF_8); - Document document = XmlUtils.parseDocument(xml, true); - recordResourceReferences(file, isDefaultFolder, document.getDocumentElement(), from); - } - - @Nullable - private Resource getResource(@NonNull ResourceType type, @NonNull String name) { - Map nameMap = typeToName.get(type); - if (nameMap != null) { - return nameMap.get(getFieldName(name)); - } - return null; - } - - @Nullable - private Resource getResource(@NonNull String possibleUrlReference) { - ResourceUrl url = ResourceUrl.parse(possibleUrlReference); - if (url != null && !url.framework) { - return getResource(url.type, url.name); - } - return null; - } - - @VisibleForTesting - @Nullable - Resource getResourceFromCode(@NonNull String owner, @NonNull String name) { - Pair> pair = resourceObfuscation.get(owner); - if (pair != null) { - ResourceType type = pair.getFirst(); - Map nameMap = pair.getSecond(); - String renamedField = nameMap.get(name); - if (renamedField != null) { - name = renamedField; - } - return getResource(type, name); - } - return null; - } - - private void recordManifestUsages(Node node) { - short nodeType = node.getNodeType(); - if (nodeType == Node.ELEMENT_NODE) { - Element element = (Element) node; - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attr = (Attr) attributes.item(i); - markReachable(getResource(attr.getValue())); - } - } else if (nodeType == Node.TEXT_NODE) { - // Does this apply to any manifests?? - String text = node.getNodeValue().trim(); - markReachable(getResource(text)); - } - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - recordManifestUsages(child); - } - } - - private void recordResourceReferences(@NonNull File file, boolean isDefaultFolder, - @NonNull Node node, @Nullable Resource from) { - short nodeType = node.getNodeType(); - if (nodeType == Node.ELEMENT_NODE) { - Element element = (Element) node; - if (from != null) { - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attr = (Attr) attributes.item(i); - Resource resource = getResource(attr.getValue()); - if (resource != null) { - from.addReference(resource); - } - } - // Android Wear. We *could* limit ourselves to only doing this in files - // referenced from a manifest meta-data element, e.g. - // - // but given that that property has "beta" in the name, it seems likely - // to change and therefore hardcoding it for that key risks breakage - // in the future. - if ("rawPathResId".equals(element.getTagName())) { - StringBuilder sb = new StringBuilder(); - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - if (child.getNodeType() == Element.TEXT_NODE - || child.getNodeType() == Element.CDATA_SECTION_NODE) { - sb.append(child.getNodeValue()); - } - } - if (sb.length() > 0) { - Resource resource = getResource(ResourceType.RAW, sb.toString().trim()); - from.addReference(resource); - } - } - } - Resource definition = getResource(element); - if (definition != null) { - from = definition; - definition.addLocation(file); - if (isDefaultFolder) { - definition.hasDefault = true; - } - } - String tagName = element.getTagName(); - if (TAG_STYLE.equals(tagName)) { - if (element.hasAttribute(ATTR_PARENT)) { - String parent = element.getAttribute(ATTR_PARENT); - if (!parent.isEmpty() && !parent.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) - && !parent.startsWith(PREFIX_ANDROID)) { - String parentStyle = parent; - if (!parentStyle.startsWith(STYLE_RESOURCE_PREFIX)) { - parentStyle = STYLE_RESOURCE_PREFIX + parentStyle; - } - Resource ps = getResource(getFieldName(parentStyle)); - if (ps != null && definition != null) { - definition.addReference(ps); - } - } - } else { - // Implicit parent styles by name - String name = getFieldName(element); - while (true) { - int index = name.lastIndexOf('_'); - if (index != -1) { - name = name.substring(0, index); - Resource ps = getResource(STYLE_RESOURCE_PREFIX + getFieldName(name)); - if (ps != null && definition != null) { - definition.addReference(ps); - } - } else { - break; - } - } - } - } - if (TAG_ITEM.equals(tagName)) { - // In style? If so the name: attribute can be a reference - if (element.getParentNode() != null - && element.getParentNode().getNodeName().equals(TAG_STYLE)) { - String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); - if (!name.isEmpty() && !name.startsWith("android:")) { - Resource resource = getResource(ResourceType.ATTR, name); - if (definition == null) { - Element style = (Element) element.getParentNode(); - definition = getResource(style); - if (definition != null) { - from = definition; - definition.addReference(resource); - } - } - } - } - } - } else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) { - String text = node.getNodeValue().trim(); - Resource textResource = getResource(getFieldName(text)); - if (textResource != null && from != null) { - from.addReference(textResource); - } - } - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - recordResourceReferences(file, isDefaultFolder, child, from); - } - } - - public static String getFieldName(@NonNull String styleName) { - return styleName.replace('.', '_').replace('-', '_').replace(':', '_'); - } - - private static void markReachable(@Nullable Resource resource) { - if (resource != null) { - resource.reachable = true; - } - } - - private Set mStrings; - private boolean mFoundGetIdentifier; - - private void referencedString(@NonNull String string) { - // See if the string is at all eligible; ignore strings that aren't - // identifiers (has java identifier chars and nothing but .:/), or are empty or too long - if (string.isEmpty() || string.length() > 80) { - return; - } - boolean haveIdentifierChar = false; - for (int i = 0, n = string.length(); i < n; i++) { - char c = string.charAt(i); - boolean identifierChar = Character.isJavaIdentifierPart(c); - if (!identifierChar && c != '.' && c != ':' && c != '/') { - // .:/ are for the fully qualified resuorce names - return; - } else if (identifierChar) { - haveIdentifierChar = true; - } - } - if (!haveIdentifierChar) { - return; - } - if (mStrings == null) { - mStrings = Sets.newHashSetWithExpectedSize(300); - } - mStrings.add(string); - } - - private void recordUsages(Path jarFile) throws IOException { - if (!jarFile.toFile().exists()) { - return; - } - ZipInputStream zis = null; - try { - FileInputStream fis = new FileInputStream(jarFile.toFile()); - try { - zis = new ZipInputStream(fis); - ZipEntry entry = zis.getNextEntry(); - while (entry != null) { - String name = entry.getName(); - if (name.endsWith(DOT_CLASS)) { - byte[] bytes = ByteStreams.toByteArray(zis); - if (bytes != null) { - ClassReader classReader = new ClassReader(bytes); - classReader.accept(new UsageVisitor(), 0); - } - } - entry = zis.getNextEntry(); - } - } finally { - Closeables.close(fis, true); - } - } finally { - Closeables.close(zis, true); - } - } - - private void parseResourceTxtFile(Path rTxt, Set resourcePackages) throws IOException { - BufferedReader reader = java.nio.file.Files.newBufferedReader(rTxt, UTF_8); - String line; - while ((line = reader.readLine()) != null) { - String[] tokens = line.split(" "); - ResourceType type = ResourceType.getEnum(tokens[1]); - for (String resourcePackage : resourcePackages) { - resourceObfuscation.put(resourcePackage.replace('.', '/') + "/R$" + type.getName(), - Pair.>of(type, Maps.newHashMap())); - } - if (type == ResourceType.STYLEABLE) { - if (tokens[0].equals("int[]")) { - addResource(ResourceType.DECLARE_STYLEABLE, tokens[2], null); - } else { - // TODO(jongerrish): Implement stripping of styleables. - } - } else { - addResource(type, tokens[2], tokens[3]); - } - } - } - - private void addResource(@NonNull ResourceType type, @NonNull String name, - @Nullable String value) { - int realValue = value != null ? Integer.decode(value) : -1; - Resource resource = getResource(type, name); - if (resource != null) { - //noinspection VariableNotUsedInsideIf - if (value != null) { - if (resource.value == -1) { - resource.value = realValue; - } else { - assert realValue == resource.value; - } - } - return; - } - resource = new Resource(type, name, realValue); - resources.add(resource); - if (realValue != -1) { - valueToResource.put(realValue, resource); - } - Map nameMap = typeToName.get(type); - if (nameMap == null) { - nameMap = Maps.newHashMapWithExpectedSize(30); - typeToName.put(type, nameMap); - } - nameMap.put(name, resource); - // TODO: Assert that we don't set the same resource multiple times to different values. - // Could happen if you pass in stale data! - } - - @VisibleForTesting - List getAllResources() { - return resources; - } - - /** - * Metadata about an Android resource - */ - public static class Resource { - - /** - * Type of resource - */ - public ResourceType type; - /** - * Name of resource - */ - public String name; - /** - * Integer id location - */ - public int value; - /** - * Whether this resource can be reached from one of the roots (manifest, code) - */ - public boolean reachable; - /** - * Whether this resource has a default definition (e.g. present in a resource folder with no - * qualifiers). For id references, an inline definition (@+id) does not count as a default - * definition. - */ - public boolean hasDefault; - /** - * Resources this resource references. For example, a layout can reference another via an - * include; a style reference in a layout references that layout style, and so on. - */ - public List references; - public final List declarations = Lists.newArrayList(); - - private Resource(ResourceType type, String name, int value) { - this.type = type; - this.name = name; - this.value = value; - } - - @Override - public String toString() { - return type + ":" + name + ":" + value; - } - - @SuppressWarnings("RedundantIfStatement") // Generated by IDE - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Resource resource = (Resource) o; - if (name != null ? !name.equals(resource.name) : resource.name != null) { - return false; - } - if (type != resource.type) { - return false; - } - return true; - } - - @Override - public int hashCode() { - int result = type != null ? type.hashCode() : 0; - result = 31 * result + (name != null ? name.hashCode() : 0); - return result; - } - - public void addLocation(@NonNull File file) { - declarations.add(file); - } - - public void addReference(@Nullable Resource resource) { - if (resource != null) { - if (references == null) { - references = Lists.newArrayList(); - } else if (references.contains(resource)) { - return; - } - references.add(resource); - } - } - - public String getUrl() { - return '@' + type.getName() + '/' + name; - } - - public boolean isRelevantType() { - return type != ResourceType.ID; // && getFolderType() != ResourceFolderType.VALUES; - } - } - - private class UsageVisitor extends ClassVisitor { - - public UsageVisitor() { - super(Opcodes.ASM5); - } - - @Override - public MethodVisitor visitMethod(int access, final String name, - String desc, String signature, String[] exceptions) { - return new MethodVisitor(Opcodes.ASM5) { - @Override - public void visitLdcInsn(Object cst) { - if (cst instanceof Integer) { - Integer value = (Integer) cst; - markReachable(valueToResource.get(value)); - } else if (cst instanceof String) { - String string = (String) cst; - referencedString(string); - } - } - - @Override - public void visitFieldInsn(int opcode, String owner, String name, String desc) { - if (opcode == Opcodes.GETSTATIC) { - Resource resource = getResourceFromCode(owner, name); - if (resource != null) { - markReachable(resource); - } - } - } - - @Override - public void visitMethodInsn( - int opcode, String owner, String name, String desc, boolean isInterface) { - super.visitMethodInsn(opcode, owner, name, desc, isInterface); - if (owner.equals("android/content/res/Resources") - && name.equals("getIdentifier") - && desc.equals("(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I")) { - mFoundGetIdentifier = true; - // TODO: Check previous instruction and see if we can find a literal - // String; if so, we can more accurately dispatch the resource here - // rather than having to check the whole string pool! - } - } - }; - } - } -} -- cgit v1.2.3