From 5c8d4274356a27a2a7a2998cdd7b0d9095c2e0b6 Mon Sep 17 00:00:00 2001 From: Andrew Pellegrini Date: Mon, 19 Dec 2016 22:07:28 +0000 Subject: Switch Bazel to use ResourceUsageAnalyzer.java instead of ResourceShrinker.java. -- PiperOrigin-RevId: 142484589 MOS_MIGRATED_REVID=142484589 --- third_party/java/aosp_gradle_core/BUILD | 20 + third_party/java/aosp_gradle_core/LICENSE | 159 +++ .../build/gradle/tasks/ResourceUsageAnalyzer.java | 1178 ++++++++++++++++++++ 3 files changed, 1357 insertions(+) create mode 100644 third_party/java/aosp_gradle_core/BUILD create mode 100644 third_party/java/aosp_gradle_core/LICENSE create mode 100644 third_party/java/aosp_gradle_core/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java (limited to 'third_party/java') diff --git a/third_party/java/aosp_gradle_core/BUILD b/third_party/java/aosp_gradle_core/BUILD new file mode 100644 index 0000000000..7df881c1a9 --- /dev/null +++ b/third_party/java/aosp_gradle_core/BUILD @@ -0,0 +1,20 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +filegroup( + name = "srcs", + srcs = glob(["**"]), +) + +filegroup(name = "embedded_aosp_gradle_core") + +java_library( + name = "aosp_gradle_core", + srcs = glob(["java/**/*.java"]), + deps = [ + "//third_party:android_common_25_0_0", + "//third_party:asm", + "//third_party:guava", + ], +) diff --git a/third_party/java/aosp_gradle_core/LICENSE b/third_party/java/aosp_gradle_core/LICENSE new file mode 100644 index 0000000000..51fe916fa3 --- /dev/null +++ b/third_party/java/aosp_gradle_core/LICENSE @@ -0,0 +1,159 @@ + + Copyright (c) 2012, The Android Open Source Project + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + 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. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS diff --git a/third_party/java/aosp_gradle_core/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java b/third_party/java/aosp_gradle_core/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java new file mode 100644 index 0000000000..f31ea6de86 --- /dev/null +++ b/third_party/java/aosp_gradle_core/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java @@ -0,0 +1,1178 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.android.build.gradle.tasks; + +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 ResourceUsageAnalyzer { + + 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 ResourceUsageAnalyzer( + 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