Add a script to inject values into manifests

Add a script that can inject a <uses-sdk minSdkVersion=""> into
AndroidManifest.xml files.  This will help with merging
LOCAL_STATIC_ANDROID_LIBRARIES, because ManifestMerger treats
a missing minSdkVersion as minSdkVersion=1 and throws errors
if libraries use a larger minSdkVersion.  It will also help
with cases where an app has a manifest that specifies an old
minSdkVersion, but the build system is compiling the app in
a way that is not compatibile with old devices, for example
using a newer dex format.

Bug: 110167203
Test: m java
Test: build/soong/scripts/manifest_fixer_test.py
Change-Id: I528d71a225feb86464c530e11b223babb0ea9edf
diff --git a/scripts/manifest_fixer.py b/scripts/manifest_fixer.py
new file mode 100755
index 0000000..f34f6c3
--- /dev/null
+++ b/scripts/manifest_fixer.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2018 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.
+#
+"""A tool for inserting values from the build system into a manifest."""
+
+from __future__ import print_function
+import argparse
+import sys
+from xml.dom import minidom
+
+
+android_ns = 'http://schemas.android.com/apk/res/android'
+
+
+def get_children_with_tag(parent, tag_name):
+  children = []
+  for child in  parent.childNodes:
+    if child.nodeType == minidom.Node.ELEMENT_NODE and \
+       child.tagName == tag_name:
+      children.append(child)
+  return children
+
+
+def parse_args():
+  """Parse commandline arguments."""
+
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--minSdkVersion', default='', dest='min_sdk_version',
+                      help='specify minSdkVersion used by the build system')
+  parser.add_argument('input', help='input AndroidManifest.xml file')
+  parser.add_argument('output', help='input AndroidManifest.xml file')
+  return parser.parse_args()
+
+
+def parse_manifest(doc):
+  """Get the manifest element."""
+
+  manifest = doc.documentElement
+  if manifest.tagName != 'manifest':
+    raise RuntimeError('expected manifest tag at root')
+  return manifest
+
+
+def ensure_manifest_android_ns(doc):
+  """Make sure the manifest tag defines the android namespace."""
+
+  manifest = parse_manifest(doc)
+
+  ns = manifest.getAttributeNodeNS(minidom.XMLNS_NAMESPACE, 'android')
+  if ns is None:
+    attr = doc.createAttributeNS(minidom.XMLNS_NAMESPACE, 'xmlns:android')
+    attr.value = android_ns
+    manifest.setAttributeNode(attr)
+  elif ns.value != android_ns:
+    raise RuntimeError('manifest tag has incorrect android namespace ' +
+                       ns.value)
+
+
+def as_int(s):
+  try:
+    i = int(s)
+  except ValueError:
+    return s, False
+  return i, True
+
+
+def compare_version_gt(a, b):
+  """Compare two SDK versions.
+
+  Compares a and b, treating codenames like 'Q' as higher
+  than numerical versions like '28'.
+
+  Returns True if a > b
+
+  Args:
+    a: value to compare
+    b: value to compare
+  Returns:
+    True if a is a higher version than b
+  """
+
+  a, a_is_int = as_int(a.upper())
+  b, b_is_int = as_int(b.upper())
+
+  if a_is_int == b_is_int:
+    # Both are codenames or both are versions, compare directly
+    return a > b
+  else:
+    # One is a codename, the other is not.  Return true if
+    # b is an integer version
+    return b_is_int
+
+
+def raise_min_sdk_version(doc, requested):
+  """Ensure the manifest contains a <uses-sdk> tag with a minSdkVersion.
+
+  Args:
+    doc: The XML document.  May be modified by this function.
+    requested: The requested minSdkVersion attribute.
+  Raises:
+    RuntimeError: invalid manifest
+  """
+
+  manifest = parse_manifest(doc)
+
+  # Get or insert the uses-sdk element
+  uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
+  if len(uses_sdk) > 1:
+    raise RuntimeError('found multiple uses-sdk elements')
+  elif len(uses_sdk) == 1:
+    element = uses_sdk[0]
+  else:
+    element = doc.createElement('uses-sdk')
+    indent = ''
+    first = manifest.firstChild
+    if first is not None and first.nodeType == minidom.Node.TEXT_NODE:
+      text = first.nodeValue
+      indent = text[:len(text)-len(text.lstrip())]
+    if not indent or indent == '\n':
+      indent = '\n    '
+
+    manifest.insertBefore(element, manifest.firstChild)
+
+    # Insert an indent before uses-sdk to line it up with the indentation of the
+    # other children of the <manifest> tag.
+    manifest.insertBefore(doc.createTextNode(indent), manifest.firstChild)
+
+  # Get or insert the minSdkVersion attribute
+  min_attr = element.getAttributeNodeNS(android_ns, 'minSdkVersion')
+  if min_attr is None:
+    min_attr = doc.createAttributeNS(android_ns, 'android:minSdkVersion')
+    min_attr.value = '1'
+    element.setAttributeNode(min_attr)
+
+  # Update the value of the minSdkVersion attribute if necessary
+  if compare_version_gt(requested, min_attr.value):
+    min_attr.value = requested
+
+
+def write_xml(f, doc):
+  f.write('<?xml version="1.0" encoding="utf-8"?>\n')
+  for node in doc.childNodes:
+    f.write(node.toxml(encoding='utf-8') + '\n')
+
+
+def main():
+  """Program entry point."""
+  try:
+    args = parse_args()
+
+    doc = minidom.parse(args.input)
+
+    ensure_manifest_android_ns(doc)
+
+    if args.min_sdk_version:
+      raise_min_sdk_version(doc, args.min_sdk_version)
+
+    with open(args.output, 'wb') as f:
+      write_xml(f, doc)
+
+  # pylint: disable=broad-except
+  except Exception as err:
+    print('error: ' + str(err), file=sys.stderr)
+    sys.exit(-1)
+
+if __name__ == '__main__':
+  main()