[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()