[Android] Add tools to find and remove unused Android resources.
BUG=149661
Review URL: https://codereview.chromium.org/13862017
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 702f311cb961fd0b4a2a34604c034d112c9b50bf
diff --git a/find_unused_resources.py b/find_unused_resources.py
new file mode 100755
index 0000000..da86e73
--- /dev/null
+++ b/find_unused_resources.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Lists unused Java strings and other resources."""
+
+import optparse
+import re
+import subprocess
+import sys
+
+
+def GetApkResources(apk_path):
+ """Returns the types and names of resources packaged in an APK.
+
+ Args:
+ apk_path: path to the APK.
+
+ Returns:
+ The resources in the APK as a list of tuples (type, name). Example:
+ [('drawable', 'arrow'), ('layout', 'month_picker'), ...]
+ """
+ p = subprocess.Popen(
+ ['aapt', 'dump', 'resources', apk_path],
+ stdout=subprocess.PIPE)
+ dump_out, _ = p.communicate()
+ assert p.returncode == 0, 'aapt dump failed'
+ matches = re.finditer(
+ r'^\s+spec resource 0x[0-9a-fA-F]+ [\w.]+:(?P<type>\w+)/(?P<name>\w+)',
+ dump_out, re.MULTILINE)
+ return [m.group('type', 'name') for m in matches]
+
+
+def GetUsedResources(source_paths, resource_types):
+ """Returns the types and names of resources used in Java or resource files.
+
+ Args:
+ source_paths: a list of files or folders collectively containing all the
+ Java files, resource files, and the AndroidManifest.xml.
+ resource_types: a list of resource types to look for. Example:
+ ['string', 'drawable']
+
+ Returns:
+ The resources referenced by the Java and resource files as a list of tuples
+ (type, name). Example:
+ [('drawable', 'app_icon'), ('layout', 'month_picker'), ...]
+ """
+ type_regex = '|'.join(map(re.escape, resource_types))
+ patterns = [r'@(())(%s)/(\w+)' % type_regex,
+ r'\b((\w+\.)*)R\.(%s)\.(\w+)' % type_regex]
+ resources = []
+ for pattern in patterns:
+ p = subprocess.Popen(
+ ['grep', '-REIhoe', pattern] + source_paths,
+ stdout=subprocess.PIPE)
+ grep_out, grep_err = p.communicate()
+ # Check stderr instead of return code, since return code is 1 when no
+ # matches are found.
+ assert not grep_err, 'grep failed'
+ matches = re.finditer(pattern, grep_out)
+ for match in matches:
+ package = match.group(1)
+ if package == 'android.':
+ continue
+ type_ = match.group(3)
+ name = match.group(4)
+ resources.append((type_, name))
+ return resources
+
+
+def FormatResources(resources):
+ """Formats a list of resources for printing.
+
+ Args:
+ resources: a list of resources, given as (type, name) tuples.
+ """
+ return '\n'.join(['%-12s %s' % (t, n) for t, n in sorted(resources)])
+
+
+def ParseArgs(args):
+ usage = 'usage: %prog [-v] APK_PATH SOURCE_PATH...'
+ parser = optparse.OptionParser(usage=usage)
+ parser.add_option('-v', help='Show verbose output', action='store_true')
+ options, args = parser.parse_args(args=args)
+ if len(args) < 2:
+ parser.error('must provide APK_PATH and SOURCE_PATH arguments')
+ return options.v, args[0], args[1:]
+
+
+def main(args=None):
+ verbose, apk_path, source_paths = ParseArgs(args)
+ apk_resources = GetApkResources(apk_path)
+ resource_types = list(set([r[0] for r in apk_resources]))
+ used_resources = GetUsedResources(source_paths, resource_types)
+ unused_resources = set(apk_resources) - set(used_resources)
+ undefined_resources = set(used_resources) - set(apk_resources)
+
+ # aapt dump fails silently. Notify the user if things look wrong.
+ if not apk_resources:
+ print >> sys.stderr, (
+ 'Warning: No resources found in the APK. Did you provide the correct '
+ 'APK path?')
+ if not used_resources:
+ print >> sys.stderr, (
+ 'Warning: No resources references from Java or resource files. Did you '
+ 'provide the correct source paths?')
+ if undefined_resources:
+ print >> sys.stderr, (
+ 'Warning: found %d "undefined" resources that are referenced by Java '
+ 'files or by other resources, but are not in the APK. Run with -v to '
+ 'see them.' % len(undefined_resources))
+
+ if verbose:
+ print '%d undefined resources:' % len(undefined_resources)
+ print FormatResources(undefined_resources), '\n'
+ print '%d resources packaged into the APK:' % len(apk_resources)
+ print FormatResources(apk_resources), '\n'
+ print '%d used resources:' % len(used_resources)
+ print FormatResources(used_resources), '\n'
+ print '%d unused resources:' % len(unused_resources)
+ print FormatResources(unused_resources)
+
+
+if __name__ == '__main__':
+ main()