aboutsummaryrefslogtreecommitdiffhomepage
path: root/third_party/java
diff options
context:
space:
mode:
authorGravatar Andrew Pellegrini <apell@google.com>2016-12-19 22:07:28 +0000
committerGravatar Kristina Chodorow <kchodorow@google.com>2016-12-19 22:11:26 +0000
commit5c8d4274356a27a2a7a2998cdd7b0d9095c2e0b6 (patch)
tree2fa7ce86cf0b6e3a912ad41dd9d9d0fa2394b98b /third_party/java
parenta7a9dbc5d07171c3010ee4297baa6cfd483ff4a6 (diff)
Switch Bazel to use ResourceUsageAnalyzer.java instead of ResourceShrinker.java.
-- PiperOrigin-RevId: 142484589 MOS_MIGRATED_REVID=142484589
Diffstat (limited to 'third_party/java')
-rw-r--r--third_party/java/aosp_gradle_core/BUILD20
-rw-r--r--third_party/java/aosp_gradle_core/LICENSE159
-rw-r--r--third_party/java/aosp_gradle_core/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java1178
3 files changed, 1357 insertions, 0 deletions
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.
+ * <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 ResourceUsageAnalyzer {
+
+ 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 ResourceUsageAnalyzer(
+ 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!
+ }
+ }
+ };
+ }
+ }
+}