aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools
diff options
context:
space:
mode:
authorGravatar Andrew Pellegrini <apell@google.com>2016-12-20 18:50:59 +0000
committerGravatar Klaus Aehlig <aehlig@google.com>2016-12-21 09:46:52 +0000
commit88c30e0b8d01ae3afd932309ca368d8426cf3d59 (patch)
tree9a11226386af17da2a7eef44f1ac660d0f6f1b94 /src/tools/android/java/com/google/devtools
parentaaee64e02fb5d110ccbed0eb903f5f2187071e97 (diff)
Delete Bazel local version ResourceShrinker.java.
-- PiperOrigin-RevId: 142573704 MOS_MIGRATED_REVID=142573704
Diffstat (limited to 'src/tools/android/java/com/google/devtools')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/ResourceShrinker.java1177
1 files changed, 0 insertions, 1177 deletions
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.
- * <p>It does this by examining
- * <ul>
- * <li>The merged manifest, to find root resource references (such as drawables used for activity
- * icons)</li>
- * <li>The R.txt file (to find the actual integer constants assigned to resources)</li>
- * <li>The ProGuard log files (to find the mapping from original symbol names to short names)</li>
- * <li>The merged resources (to find which resources reference other resources, e.g. drawable
- * state lists including other drawables, or layouts including other layouts, or styles
- * referencing other drawables, or menus items including action layouts, etc.)</li>
- * <li>The ProGuard output classes (to find resource references in code that are actually
- * reachable)</li>
- * </ul>
- * 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.
- * <p>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 <b>do</b> find one, we check <b>all</b> 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 <b>all</b> 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. <p> 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<String> 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<Resource> unused;
- /**
- * List of all known resources (parsed from R.java)
- */
- private List<Resource> resources = Lists.newArrayListWithExpectedSize(TYPICAL_RESOURCE_COUNT);
- /**
- * Map from R field value to corresponding resource
- */
- private Map<Integer, Resource> valueToResource =
- Maps.newHashMapWithExpectedSize(TYPICAL_RESOURCE_COUNT);
- /**
- * Map from resource type to map from resource name to resource object
- */
- private Map<ResourceType, Map<String, Resource>> 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<String, Pair<ResourceType, Map<String, String>>> resourceObfuscation =
- Maps.newHashMapWithExpectedSize(30);
-
- public ResourceShrinker(
- Set<String> 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)}).
- *
- * <p>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<File> skip = Sets.newHashSetWithExpectedSize(resourceCount);
- Set<File> rewrite = Sets.newHashSetWithExpectedSize(resourceCount);
- Set<Resource> 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<File, String> 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<File> rewrite, Map<File, String> 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<String> 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<File, String> rewritten)
- throws IOException, ParserConfigurationException, SAXException {
- if (values.exists()) {
- String xml = rewritten.get(values);
- if (xml == null) {
- xml = Files.toString(values, UTF_8);
- }
- List<String> 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<Resource> deleted,
- Map<File, String> 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<File> skip,
- Map<File, String> 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<String> 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<Resource> 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<Resource, Boolean> seen = new IdentityHashMap<>(resources.size());
- for (Resource root : roots) {
- visit(root, seen);
- }
- List<Resource> 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<Resource, Boolean> 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<String> strings = new ArrayList<String>(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<String> names = Sets.newHashSetWithExpectedSize(50);
- for (Map<String, Resource> 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<String, Resource> 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<ResourceType> 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<String, String> 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<ResourceType, Map<String, String>> 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<String, Resource> 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<ResourceType, Map<String, String>> pair = resourceObfuscation.get(owner);
- if (pair != null) {
- ResourceType type = pair.getFirst();
- Map<String, String> 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.
- // <meta-data android:name="com.google.android.wearable.beta.app"
- // android:resource="@xml/wearable_app_desc"/>
- // 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<String> 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<String> 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.<ResourceType, Map<String, String>>of(type, Maps.<String, String>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<String, Resource> 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<Resource> 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<Resource> references;
- public final List<File> 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!
- }
- }
- };
- }
- }
-}