aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools
diff options
context:
space:
mode:
authorGravatar Alex Humesky <ahumesky@google.com>2015-05-26 22:23:13 +0000
committerGravatar Laurent Le Brun <laurentlb@google.com>2015-05-27 16:46:05 +0000
commitb46c6351321c2950badb4c0e4071e3f20d49e338 (patch)
tree59710d4c8b959c1aa1ac093b1311e905745c03e9 /tools
parentb2053d796acf4c1bc39ddc56769be22a9e09c0da (diff)
Adds tools for building Android apps.
-- MOS_MIGRATED_REVID=94515805
Diffstat (limited to 'tools')
-rw-r--r--tools/android/BUILD43
-rw-r--r--tools/android/android_permissions.py146
-rw-r--r--tools/android/merge_manifests.py422
-rw-r--r--tools/android/merge_manifests_test.py509
-rw-r--r--tools/android/proguard_whitelister.py73
-rw-r--r--tools/android/proguard_whitelister_input.cfg48
-rw-r--r--tools/android/proguard_whitelister_test.py57
7 files changed, 1293 insertions, 5 deletions
diff --git a/tools/android/BUILD b/tools/android/BUILD
index bc8e1544f4..9b64988475 100644
--- a/tools/android/BUILD
+++ b/tools/android/BUILD
@@ -1,14 +1,50 @@
+package(default_visibility = ["//visibility:public"])
+
+py_binary(
+ name = "merge_manifests",
+ srcs = [
+ "android_permissions.py",
+ "merge_manifests.py",
+ ],
+ deps = [
+ "//third_party/py/gflags",
+ ],
+)
+
+py_test(
+ name = "merge_manifests_test",
+ srcs = ["merge_manifests_test.py"],
+ deps = [":merge_manifests"],
+)
+
+py_binary(
+ name = "proguard_whitelister",
+ srcs = [
+ "proguard_whitelister.py",
+ ],
+ deps = [
+ "//third_party/py/gflags",
+ ],
+)
+
+py_test(
+ name = "proguard_whitelister_test",
+ srcs = ["proguard_whitelister_test.py"],
+ data = ["proguard_whitelister_input.cfg"],
+ deps = [
+ ":proguard_whitelister",
+ ],
+)
+
py_binary(
name = "build_incremental_dexmanifest",
srcs = [":build_incremental_dexmanifest.py"],
- visibility = ["//visibility:public"],
deps = [],
)
py_binary(
name = "build_split_manifest",
srcs = ["build_split_manifest.py"],
- visibility = ["//visibility:public"],
deps = [
"//third_party/py/gflags",
],
@@ -25,7 +61,6 @@ py_test(
py_binary(
name = "incremental_install",
srcs = ["incremental_install.py"],
- visibility = ["//visibility:public"],
deps = [
"//third_party/py/concurrent:futures",
"//third_party/py/gflags",
@@ -44,7 +79,6 @@ py_test(
py_binary(
name = "strip_resources",
srcs = ["strip_resources.py"],
- visibility = ["//visibility:public"],
deps = [
"//third_party/py/gflags",
],
@@ -53,7 +87,6 @@ py_binary(
py_binary(
name = "stubify_manifest",
srcs = ["stubify_manifest.py"],
- visibility = ["//visibility:public"],
deps = [
"//third_party/py/gflags",
],
diff --git a/tools/android/android_permissions.py b/tools/android/android_permissions.py
new file mode 100644
index 0000000000..be54bda403
--- /dev/null
+++ b/tools/android/android_permissions.py
@@ -0,0 +1,146 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""List of valid android permissions as they are for android version 4.0.3.
+
+ This list is to be used by manifest merge to verify the exclude_permission
+ values.
+"""
+
+PERMISSIONS = frozenset([
+ 'android.permission.ACCESS_CHECKIN_PROPERTIES',
+ 'android.permission.ACCESS_COARSE_LOCATION',
+ 'android.permission.ACCESS_FINE_LOCATION',
+ 'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS',
+ 'android.permission.ACCESS_MOCK_LOCATION',
+ 'android.permission.ACCESS_NETWORK_STATE',
+ 'android.permission.ACCESS_SURFACE_FLINGER',
+ 'android.permission.ACCESS_WIFI_STATE',
+ 'android.permission.ACCOUNT_MANAGER',
+ 'android.permission.AUTHENTICATE_ACCOUNTS',
+ 'android.permission.BATTERY_STATS',
+ 'android.permission.BIND_APPWIDGET',
+ 'android.permission.BIND_DEVICE_ADMIN',
+ 'android.permission.BIND_INPUT_METHOD',
+ 'android.permission.BIND_REMOTEVIEWS',
+ 'android.permission.BIND_TEXT_SERVICE',
+ 'android.permission.BIND_VPN_SERVICE',
+ 'android.permission.BIND_WALLPAPER',
+ 'android.permission.BLUETOOTH',
+ 'android.permission.BLUETOOTH_ADMIN',
+ 'android.permission.BRICK',
+ 'android.permission.BROADCAST_PACKAGE_REMOVED',
+ 'android.permission.BROADCAST_SMS',
+ 'android.permission.BROADCAST_STICKY',
+ 'android.permission.BROADCAST_WAP_PUSH',
+ 'android.permission.CALL_PHONE',
+ 'android.permission.CALL_PRIVILEGED',
+ 'android.permission.CAMERA',
+ 'android.permission.CHANGE_COMPONENT_ENABLED_STATE',
+ 'android.permission.CHANGE_CONFIGURATION',
+ 'android.permission.CHANGE_NETWORK_STATE',
+ 'android.permission.CHANGE_WIFI_MULTICAST_STATE',
+ 'android.permission.CHANGE_WIFI_STATE',
+ 'android.permission.CLEAR_APP_CACHE',
+ 'android.permission.CLEAR_APP_USER_DATA',
+ 'android.permission.CONTROL_LOCATION_UPDATES',
+ 'android.permission.DELETE_CACHE_FILES',
+ 'android.permission.DELETE_PACKAGES',
+ 'android.permission.DEVICE_POWER',
+ 'android.permission.DIAGNOSTIC',
+ 'android.permission.DISABLE_KEYGUARD',
+ 'android.permission.DUMP',
+ 'android.permission.EXPAND_STATUS_BAR',
+ 'android.permission.FACTORY_TEST',
+ 'android.permission.FLASHLIGHT',
+ 'android.permission.FORCE_BACK',
+ 'android.permission.GET_ACCOUNTS',
+ 'android.permission.GET_PACKAGE_SIZE',
+ 'android.permission.GET_TASKS',
+ 'android.permission.GLOBAL_SEARCH',
+ 'android.permission.HARDWARE_TEST',
+ 'android.permission.INJECT_EVENTS',
+ 'android.permission.INSTALL_LOCATION_PROVIDER',
+ 'android.permission.INSTALL_PACKAGES',
+ 'android.permission.INTERNAL_SYSTEM_WINDOW',
+ 'android.permission.INTERNET',
+ 'android.permission.KILL_BACKGROUND_PROCESSES',
+ 'android.permission.MANAGE_ACCOUNTS',
+ 'android.permission.MANAGE_APP_TOKENS',
+ 'android.permission.MASTER_CLEAR',
+ 'android.permission.MODIFY_AUDIO_SETTINGS',
+ 'android.permission.MODIFY_PHONE_STATE',
+ 'android.permission.MOUNT_FORMAT_FILESYSTEMS',
+ 'android.permission.MOUNT_UNMOUNT_FILESYSTEMS',
+ 'android.permission.NFC',
+ 'android.permission.PERSISTENT_ACTIVITY',
+ 'android.permission.PROCESS_OUTGOING_CALLS',
+ 'android.permission.READ_CALENDAR',
+ 'android.permission.READ_CONTACTS',
+ 'android.permission.READ_FRAME_BUFFER',
+ 'android.permission.READ_INPUT_STATE',
+ 'android.permission.READ_LOGS',
+ 'android.permission.READ_PHONE_STATE',
+ 'android.permission.READ_PROFILE',
+ 'android.permission.READ_SMS',
+ 'android.permission.READ_SOCIAL_STREAM',
+ 'android.permission.READ_SYNC_SETTINGS',
+ 'android.permission.READ_SYNC_STATS',
+ 'android.permission.REBOOT',
+ 'android.permission.RECEIVE_BOOT_COMPLETED',
+ 'android.permission.RECEIVE_MMS',
+ 'android.permission.RECEIVE_SMS',
+ 'android.permission.RECEIVE_WAP_PUSH',
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.REORDER_TASKS',
+ 'android.permission.RESTART_PACKAGES',
+ 'android.permission.SEND_SMS',
+ 'android.permission.SET_ACTIVITY_WATCHER',
+ 'android.permission.SET_ALWAYS_FINISH',
+ 'android.permission.SET_ANIMATION_SCALE',
+ 'android.permission.SET_DEBUG_APP',
+ 'android.permission.SET_ORIENTATION',
+ 'android.permission.SET_POINTER_SPEED',
+ 'android.permission.SET_PREFERRED_APPLICATIONS',
+ 'android.permission.SET_PROCESS_LIMIT',
+ 'android.permission.SET_TIME',
+ 'android.permission.SET_TIME_ZONE',
+ 'android.permission.SET_WALLPAPER',
+ 'android.permission.SET_WALLPAPER_HINTS',
+ 'android.permission.SIGNAL_PERSISTENT_PROCESSES',
+ 'android.permission.STATUS_BAR',
+ 'android.permission.SUBSCRIBED_FEEDS_READ',
+ 'android.permission.SUBSCRIBED_FEEDS_WRITE',
+ 'android.permission.SYSTEM_ALERT_WINDOW',
+ 'android.permission.UPDATE_DEVICE_STATS',
+ 'android.permission.USE_CREDENTIALS',
+ 'android.permission.USE_SIP',
+ 'android.permission.VIBRATE',
+ 'android.permission.WAKE_LOCK',
+ 'android.permission.WRITE_APN_SETTINGS',
+ 'android.permission.WRITE_CALENDAR',
+ 'android.permission.WRITE_CONTACTS',
+ 'android.permission.WRITE_EXTERNAL_STORAGE',
+ 'android.permission.WRITE_GSERVICES',
+ 'android.permission.WRITE_PROFILE',
+ 'android.permission.WRITE_SECURE_SETTINGS',
+ 'android.permission.WRITE_SETTINGS',
+ 'android.permission.WRITE_SMS',
+ 'android.permission.WRITE_SOCIAL_STREAM',
+ 'android.permission.WRITE_SYNC_SETTINGS',
+ 'com.android.alarm.permission.SET_ALARM',
+ 'com.android.browser.permission.READ_HISTORY_BOOKMARKS',
+ 'com.android.browser.permission.WRITE_HISTORY_BOOKMARKS',
+ 'com.android.voicemail.permission.ADD_VOICEMAIL'])
+
diff --git a/tools/android/merge_manifests.py b/tools/android/merge_manifests.py
new file mode 100644
index 0000000000..59dccfed05
--- /dev/null
+++ b/tools/android/merge_manifests.py
@@ -0,0 +1,422 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Merges two android manifest xml files."""
+
+import re
+import sys
+import xml.dom.minidom
+
+from tools.android import android_permissions
+from third_party.py import gflags
+
+FLAGS = gflags.FLAGS
+EXCLUDE_ALL_ARG = 'all'
+
+gflags.DEFINE_multistring(
+ 'exclude_permission', None,
+ 'Permissions to be excluded, e.g.: "android.permission.READ_LOGS".'
+ 'This is a multistring, so multiple of those flags can be provided.'
+ 'Pass "%s" to exclude all permissions contributed by mergees.'
+ % EXCLUDE_ALL_ARG)
+gflags.DEFINE_multistring(
+ 'mergee', None,
+ 'Mergee manifest that will be merged to merger manifest.'
+ 'This is a multistring, so multiple of those flags can be provided.')
+gflags.DEFINE_string('merger', None,
+ 'Merger AndroidManifest file to be merged.')
+gflags.DEFINE_string('output', None, 'Output file with merged manifests.')
+
+USAGE = """Error, invalid arguments.
+Usage: merge_manifests.py --merger=<merger> --mergee=<mergee1> --mergee=<merge2>
+ --exclude_permission=[Exclude permissions from mergee] --output=<output>
+Examples:
+ merge_manifests.py --merger=manifest.xml --mergee=manifest2.xml
+ --mergee=manifest3.xml --exclude_permission=android.permission.READ_LOGS
+ --output=AndroidManifest.xml
+
+ merge_manifests.py --merger=manifest.xml --mergee=manifest2.xml
+ --mergee=manifest3.xml --exclude_permission=%s
+ --output=AndroidManifest.xml
+""" % EXCLUDE_ALL_ARG
+
+
+class UndefinedPlaceholderException(Exception):
+ """Exception thrown when encountering a placeholder without a replacement.
+ """
+ pass
+
+
+class MalformedManifestException(Exception):
+ """Exception thrown when encountering a fatally malformed manifest.
+ """
+ pass
+
+
+class MergeManifests(object):
+ """A utility class for merging two android manifest.xml files.
+
+ This is useful when including another app as android library.
+ """
+ _ACTIVITY = 'activity'
+ _ANDROID_NAME = 'android:name'
+ _ANDROID_LABEL = 'android:label'
+ _INTENT_FILTER = 'intent-filter'
+ _MANIFEST = 'manifest'
+ _USES_PERMISSION = 'uses-permission'
+ _NODES_TO_COPY_FROM_MERGEE = {
+ _MANIFEST: [
+ 'instrumentation',
+ 'permission',
+ _USES_PERMISSION,
+ 'uses-feature',
+ 'permission-group',
+ ],
+ 'application': [
+ 'activity',
+ 'activity-alias',
+ 'provider',
+ 'receiver',
+ 'service',
+ 'uses-library',
+ 'meta-data',
+ ],
+ }
+ _NODES_TO_REMOVE_FROM_MERGER = []
+ _PACKAGE = 'package'
+
+ def __init__(self, merger, mergees, exclude_permissions=None):
+ """Constructs and initializes the MergeManifests object.
+
+ Args:
+ merger: First (merger) AndroidManifest.xml string.
+ mergees: mergee AndroidManifest.xml strings, a list.
+ exclude_permissions: Permissions to be excludeed from merging,
+ string list. "all" means don't include any permissions.
+ """
+ self._merger = merger
+ self._mergees = mergees
+ self._exclude_permissions = exclude_permissions
+ self._merger_dom = xml.dom.minidom.parseString(self._merger[0])
+
+ def _ApplyExcludePermissions(self, dom):
+ """Apply exclude filters.
+
+ Args:
+ dom: Document dom object from which to exclude permissions.
+ """
+ if self._exclude_permissions:
+ exclude_all_permissions = EXCLUDE_ALL_ARG in self._exclude_permissions
+ for element in dom.getElementsByTagName(self._USES_PERMISSION):
+ if element.hasAttribute(self._ANDROID_NAME):
+ attrib = element.getAttribute(self._ANDROID_NAME)
+ if exclude_all_permissions or attrib in self._exclude_permissions:
+ element.parentNode.removeChild(element)
+
+ def _ExpandPackageName(self, node):
+ """Set the package name if it is in a short form.
+
+ Filtering logic for what elements have package expansion:
+ If the name starts with a dot, always prefix it with the package.
+ If the name has a dot anywhere else, do not prefix it.
+ If the name has no dot at all, also prefix it with the package.
+
+ The massageManifest function shows where this rule is applied:
+
+ In the application element, on the name and backupAgent attributes.
+ In the activity, service, receiver, provider, and activity-alias elements,
+ on the name attribute.
+ In the activity-alias element, on the targetActivity attribute.
+
+ Args:
+ node: Xml Node for which to expand package name.
+ """
+ package_name = node.getElementsByTagName(self._MANIFEST).item(
+ 0).getAttribute(self._PACKAGE)
+
+ if not package_name:
+ return
+
+ for element in node.getElementsByTagName('*'):
+ if element.nodeName not in [
+ 'activity',
+ 'activity-alias',
+ 'application',
+ 'service',
+ 'receiver',
+ 'provider',
+ ]:
+ continue
+
+ self._ExpandPackageNameHelper(package_name, element, self._ANDROID_NAME)
+
+ if element.nodeName == 'activity':
+ self._ExpandPackageNameHelper(package_name, element,
+ 'android:parentActivityName')
+
+ if element.nodeName == 'activity-alias':
+ self._ExpandPackageNameHelper(package_name, element,
+ 'android:targetActivity')
+ continue
+
+ if element.nodeName == 'application':
+ self._ExpandPackageNameHelper(package_name, element,
+ 'android:backupAgent')
+
+ def _ExpandPackageNameHelper(self, package_name, element, attribute_name):
+ if element.hasAttribute(attribute_name):
+ class_name = element.getAttribute(attribute_name)
+
+ if class_name.startswith('.'):
+ pass
+ elif '.' not in class_name:
+ class_name = '.' + class_name
+ else:
+ return
+
+ element.setAttribute(attribute_name, package_name + class_name)
+
+ def _RemoveFromMerger(self):
+ """Remove from merger."""
+ for tag_name in self._NODES_TO_REMOVE_FROM_MERGER:
+ elements = self._merger_dom.getElementsByTagName(tag_name)
+ for element in elements:
+ element.parentNode.removeChild(element)
+
+ def _RemoveAndroidLabel(self, node):
+ """Remove android:label.
+
+ We do this because it is not required by merger manifest,
+ and it might contain @string references that will not allow compilation.
+
+ Args:
+ node: Node for which to remove Android labels.
+ """
+ if node.hasAttribute(self._ANDROID_LABEL):
+ node.removeAttribute(self._ANDROID_LABEL)
+
+ def _IsDuplicate(self, node_to_copy, node):
+ """Is element a duplicate?"""
+ for merger_node in self._merger_dom.getElementsByTagName(node_to_copy):
+ if (merger_node.getAttribute(self._ANDROID_NAME) ==
+ node.getAttribute(self._ANDROID_NAME)):
+ return True
+ return False
+
+ def _RemoveIntentFilters(self, node):
+ """Remove intent-filter in activity element.
+
+ So there are no duplicate apps.
+
+ Args:
+ node: Node for which to remove intent filters.
+ """
+ intent_filters = node.getElementsByTagName(self._INTENT_FILTER)
+ if intent_filters.length > 0:
+ for sub_node in intent_filters:
+ node.removeChild(sub_node)
+
+ def _FindElementComment(self, node):
+ """Find element's comment.
+
+ Assumes that element's comment can be just above the element.
+ Searches previous siblings and looks for the first non text element
+ that is of a nodeType of comment node.
+
+ Args:
+ node: Node for which to find a comment.
+ Returns:
+ Elements's comment node, None if not found.
+ """
+ while node.previousSibling:
+ node = node.previousSibling
+ if node.nodeType is node.COMMENT_NODE:
+ return node
+ if node.nodeType is not node.TEXT_NODE:
+ return None
+ return None
+
+ def _ReplaceArgumentPlaceholders(self, dom):
+ """Replaces argument placeholders with their values.
+
+ Modifies the attribute values of the input node.
+
+ Args:
+ dom: Xml node that should get placeholders replaced.
+ """
+
+ placeholders = {
+ 'packageName': self._merger_dom.getElementsByTagName(
+ self._MANIFEST).item(0).getAttribute(self._PACKAGE),
+ }
+
+ for element in dom.getElementsByTagName('*'):
+ for i in range(element.attributes.length):
+ attr = element.attributes.item(i)
+ attr.value = self._ReplaceArgumentHelper(placeholders, attr.value)
+
+ def _ReplaceArgumentHelper(self, placeholders, attr):
+ """Replaces argument placeholders within a single string.
+
+ Args:
+ placeholders: A dict mapping between placeholder names and their
+ replacement values.
+ attr: A string in which to replace argument placeholders.
+
+ Returns:
+ A string with placeholders replaced, or the same string if no placeholders
+ were found.
+ """
+ match_placeholder = '\\${([a-zA-Z]*)}'
+
+ # Returns the replacement string for found matches.
+ def PlaceholderReplacer(matchobj):
+ found_placeholder = matchobj.group(1)
+ if found_placeholder not in placeholders:
+ raise UndefinedPlaceholderException(
+ 'Undefined placeholder when substituting arguments: '
+ + found_placeholder)
+ return placeholders[found_placeholder]
+
+ attr = re.sub(match_placeholder, PlaceholderReplacer, attr)
+
+ return attr
+
+ def _SortAliases(self):
+ applications = self._merger_dom.getElementsByTagName('application')
+ if not applications:
+ return
+ for alias in applications[0].getElementsByTagName('activity-alias'):
+ comment_node = self._FindElementComment(alias)
+ while comment_node is not None:
+ applications[0].appendChild(comment_node)
+ comment_node = self._FindElementComment(alias)
+ applications[0].appendChild(alias)
+
+ def _FindMergerParent(self, tag_to_copy, destination_tag_name, mergee_dom):
+ """Finds merger parent node, or appends mergee equivalent node if none."""
+ # Merger parent element to which to add merged elements.
+ if self._merger_dom.getElementsByTagName(destination_tag_name):
+ return self._merger_dom.getElementsByTagName(destination_tag_name)[0]
+ else:
+ mergee_element = mergee_dom.getElementsByTagName(destination_tag_name)[0]
+ # find the parent
+ parents = self._merger_dom.getElementsByTagName(
+ mergee_element.parentNode.tagName)
+ if not parents:
+ raise MalformedManifestException(
+ 'Malformed manifest has tag %s but no parent tag %s',
+ (tag_to_copy, destination_tag_name))
+ # append the mergee child as the first child.
+ return parents[0].insertBefore(mergee_element, parents[0].firstChild)
+
+ def Merge(self):
+ """Takes two manifests, and merges them together to produce a third."""
+ self._RemoveFromMerger()
+ self._ExpandPackageName(self._merger_dom)
+
+ for dom, filename in self._mergees:
+ mergee_dom = xml.dom.minidom.parseString(dom)
+ self._ReplaceArgumentPlaceholders(mergee_dom)
+ self._ExpandPackageName(mergee_dom)
+ self._ApplyExcludePermissions(mergee_dom)
+
+ for destination, values in sorted(
+ self._NODES_TO_COPY_FROM_MERGEE.iteritems()):
+ for node_to_copy in values:
+ for node in mergee_dom.getElementsByTagName(node_to_copy):
+ if self._IsDuplicate(node_to_copy, node):
+ continue
+
+ merger_parent = self._FindMergerParent(node_to_copy,
+ destination,
+ mergee_dom)
+
+ # Append the merge comment.
+ merger_parent.appendChild(
+ self._merger_dom.createComment(' Merged from file: %s ' %
+ filename))
+
+ # Append mergee's comment, if present.
+ comment_node = self._FindElementComment(node)
+ if comment_node:
+ merger_parent.appendChild(comment_node)
+
+ # Append element from mergee to merger.
+ merger_parent.appendChild(node)
+
+ # Insert top level comment about the merge.
+ top_comment = (
+ ' *** WARNING *** DO NOT EDIT! THIS IS GENERATED MANIFEST BY '
+ 'MERGE_MANIFEST TOOL.\n'
+ ' Merger manifest:\n %s\n' % self._merger[1] +
+ ' Mergee manifests:\n%s' % '\n'.join(
+ [' %s' % mergee[1] for mergee in self._mergees]) +
+ '\n ')
+ manifest_element = self._merger_dom.getElementsByTagName('manifest')[0]
+ manifest_element.insertBefore(self._merger_dom.createComment(top_comment),
+ manifest_element.firstChild)
+
+ self._SortAliases()
+ return self._merger_dom.toprettyxml(indent=' ')
+
+
+def _ReadFiles(files):
+ results = []
+ for file_name in files:
+ results.append(_ReadFile(file_name))
+ return results
+
+
+def _ReadFile(file_name):
+ with open(file_name, 'r') as my_file:
+ return (my_file.read(), file_name,)
+
+
+def _ValidateAndWarnPermissions(exclude_permissions):
+ unknown_permissions = (
+ set(exclude_permissions)
+ - set([EXCLUDE_ALL_ARG])
+ - android_permissions.PERMISSIONS)
+ return '\n'.join([
+ 'WARNING:\n\t Specified permission "%s" is not a standard permission. '
+ 'Is it a typo?' % perm for perm in unknown_permissions])
+
+
+def main():
+ if not FLAGS.merger:
+ raise RuntimeError('Missing merger value.\n' + USAGE)
+ if len(FLAGS.mergee) < 1:
+ raise RuntimeError('Missing mergee value.\n' + USAGE)
+ if not FLAGS.output:
+ raise RuntimeError('Missing output value.\n' + USAGE)
+ if FLAGS.exclude_permission:
+ warning = _ValidateAndWarnPermissions(FLAGS.exclude_permission)
+ if warning:
+ print warning
+
+ merged_manifests = MergeManifests(_ReadFile(FLAGS.merger),
+ _ReadFiles(FLAGS.mergee),
+ FLAGS.exclude_permission
+ ).Merge()
+
+ with open(FLAGS.output, 'w') as out_file:
+ for line in merged_manifests.split('\n'):
+ if not line.strip():
+ continue
+ out_file.write(line + '\n')
+
+if __name__ == '__main__':
+ FLAGS(sys.argv)
+ main()
diff --git a/tools/android/merge_manifests_test.py b/tools/android/merge_manifests_test.py
new file mode 100644
index 0000000000..349eee0119
--- /dev/null
+++ b/tools/android/merge_manifests_test.py
@@ -0,0 +1,509 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""This file contains unit tests for the merge_manifests script."""
+
+import re
+import unittest
+import xml.dom.minidom
+
+from tools.android import merge_manifests
+
+FIRST_MANIFEST = """<?xml version='1.0' encoding='utf-8'?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.apps.testapp"
+ android:versionCode="70"
+ android:versionName="1.0">
+ <uses-sdk android:minSdkVersion="10"/>
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <application
+ android:icon="@drawable/icon"
+ android:name="com.google.android.apps.testapp.TestApplication"
+ android:theme="@style/Theme.Test"
+ android:label="@string/app_name">
+ <!-- START LIBRARIES (Maintain Alphabetic order) -->
+ <!-- NFC extras -->
+ <uses-library android:name="com.google.android.nfc_extras" android:required="false"/>
+ <!-- END LIBRARIES -->
+ <!-- START ACTIVITIES (Maintain Alphabetic order) -->
+ <!-- Entry point activity - navigation and title bar. -->
+ <activity
+ android:name=".entrypoint.EntryPointActivityGroup"
+ android:screenOrientation="portrait"
+ android:launchMode="singleTop"/>
+ <activity android:name=".ui.topup.TopUpActivity" />
+ <service android:name=".nfcevent.NfcEventService" />
+ <receiver
+ android:name="com.receiver.TestReceiver"
+ android:process="@string/receiver_service_name">
+ <!-- Receive the actual message -->
+ <intent-filter>
+ <action
+ android:name="android.intent.action.USER_PRESENT"/>
+ </intent-filter>
+ </receiver>
+ <provider
+ android:name=".dataaccess.persistence.ContentProvider"
+ android:authorities="com.google.android.apps.testapp"
+ android:exported="false" />
+ </application>
+</manifest>
+"""
+
+SECOND_MANIFEST = """<?xml version='1.0' encoding='utf-8'?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.apps.testapp2"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <permission android:name="com.google.android.apps.foo.C2D_MESSAGE"
+ android:protectionLevel="signature" />
+ <uses-sdk android:minSdkVersion="5" />
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
+ <uses-permission android:name="android.permission.READ_LOGS" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <!-- Comment for permission android.permission.GET_ACCOUNTS.
+ This is just to make sure the comment is being merged correctly.
+ -->
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+ <application
+ android:icon="@drawable/icon"
+ android:name="com.google.android.apps.testapp.TestApplication2"
+ android:theme="@style/Theme.Test2"
+ android:label="@string/app_name"
+ android:backupAgent="FooBar">
+ <activity android:name=".ui.home.HomeActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity android:name=".TestActivity2"></activity>
+ <activity android:name=".PreviewActivity"></activity>
+ <activity android:name=".ShowTextActivity" android:excludeFromRecents="true"></activity>
+ <activity android:name=".ShowStringListActivity"
+ android:excludeFromRecents="true"
+ android:parentActivityName=".ui.home.HomeActivity">
+ </activity>
+ <service android:name=".TestService">
+ <meta-data android:name="param" android:value="value"/>
+ </service>
+ <service android:name=".nfcevent.NfcEventService" />
+ <receiver android:name=".ConnectivityReceiver" android:enabled="false" >
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ </receiver>
+ <activity-alias android:name="BarFoo" android:targetActivity=".FooBar" />
+ <provider
+ android:name="some.package.with.inner.class$AnInnerClass" />
+ <provider
+ android:name="${packageName}"
+ android:authorities="${packageName}.${packageName}"
+ android:exported="false" />
+ <provider
+ android:name="${packageName}.PlaceHolderProviderName"
+ android:authorities="PlaceHolderProviderAuthorities.${packageName}"
+ android:exported="false" />
+ <activity
+ android:name="activityPrefix.${packageName}.activitySuffix">
+ <intent-filter>
+ <action android:name="actionPrefix.${packageName}.actionSuffix" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
+"""
+
+THIRD_MANIFEST = """<?xml version='1.0' encoding='utf-8'?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.apps.testapp3"
+ android:versionCode="3"
+ android:versionName="1.30">
+ <uses-sdk android:minSdkVersion="14" />
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
+ <uses-permission android:name="android.permission.READ_LOGS" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application>
+ <activity android:name=".ui.home.HomeActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity android:name="TestActivity"></activity>
+ <service android:name=".TestService" />
+ <receiver android:name=".ConnectivityReceiver" android:enabled="true" >
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGER" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
+"""
+
+MANUALLY_MERGED = """<?xml version='1.0' encoding='utf-8'?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.apps.testapp"
+ android:versionCode="70"
+ android:versionName="1.0">
+ <!-- *** WARNING *** DO NOT EDIT! THIS IS GENERATED MANIFEST BY MERGE_MANIFEST TOOL.
+ Merger manifest:
+ FIRST_MANIFEST
+ Mergee manifests:
+ SECOND_MANIFEST
+ THIRD_MANIFEST
+ -->
+ <uses-sdk android:minSdkVersion="10"/>
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+ <application
+ android:icon="@drawable/icon"
+ android:name="com.google.android.apps.testapp.TestApplication"
+ android:theme="@style/Theme.Test"
+ android:label="@string/app_name">
+ <!-- START LIBRARIES (Maintain Alphabetic order) -->
+ <!-- NFC extras -->
+ <uses-library android:name="com.google.android.nfc_extras" android:required="false"/>
+ <!-- END LIBRARIES -->
+ <!-- START ACTIVITIES (Maintain Alphabetic order) -->
+ <!-- Entry point activity - navigation and title bar. -->
+ <activity
+ android:name="com.google.android.apps.testapp.entrypoint.EntryPointActivityGroup"
+ android:screenOrientation="portrait"
+ android:launchMode="singleTop"/>
+ <activity android:name="com.google.android.apps.testapp.ui.topup.TopUpActivity" />
+ <service android:name="com.google.android.apps.testapp.nfcevent.NfcEventService" />
+ <receiver
+ android:name="com.receiver.TestReceiver"
+ android:process="@string/receiver_service_name">
+ <!-- Receive the actual message -->
+ <intent-filter>
+ <action
+ android:name="android.intent.action.USER_PRESENT"/>
+ </intent-filter>
+ </receiver>
+ <provider android:authorities="com.google.android.apps.testapp" android:exported="false"
+ android:name="com.google.android.apps.testapp.dataaccess.persistence.ContentProvider"/>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:label="@string/app_name" android:name="com.google.android.apps.testapp2.ui.home.HomeActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:name="com.google.android.apps.testapp2.TestActivity2"></activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:name="com.google.android.apps.testapp2.PreviewActivity"></activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:name="com.google.android.apps.testapp2.ShowTextActivity"
+ android:excludeFromRecents="true"></activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:name="com.google.android.apps.testapp2.ShowStringListActivity"
+ android:excludeFromRecents="true"
+ android:parentActivityName="com.google.android.apps.testapp2.ui.home.HomeActivity">
+ </activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity
+ android:name="activityPrefix.com.google.android.apps.testapp.activitySuffix">
+ <intent-filter>
+ <action android:name="actionPrefix.com.google.android.apps.testapp.actionSuffix" />
+ </intent-filter>
+ </activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <provider
+ android:name="some.package.with.inner.class$AnInnerClass" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <provider
+ android:name="com.google.android.apps.testapp"
+ android:authorities="com.google.android.apps.testapp.com.google.android.apps.testapp"
+ android:exported="false" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <provider
+ android:name="com.google.android.apps.testapp.PlaceHolderProviderName"
+ android:authorities="PlaceHolderProviderAuthorities.com.google.android.apps.testapp"
+ android:exported="false" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <receiver android:name="com.google.android.apps.testapp2.ConnectivityReceiver"
+ android:enabled="false" >
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ </receiver>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <service android:name="com.google.android.apps.testapp2.TestService">
+ <meta-data android:name="param" android:value="value"/>
+ </service>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <service android:name="com.google.android.apps.testapp2.nfcevent.NfcEventService"/>
+ <!-- Merged from file: THIRD_MANIFEST -->
+ <activity android:label="@string/app_name" android:name="com.google.android.apps.testapp3.ui.home.HomeActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <!-- Merged from file: THIRD_MANIFEST -->
+ <activity android:name="com.google.android.apps.testapp3.TestActivity"/>
+ <!-- Merged from file: THIRD_MANIFEST -->
+ <receiver android:enabled="true"
+ android:name="com.google.android.apps.testapp3.ConnectivityReceiver">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGER"/>
+ </intent-filter>
+ </receiver>
+ <!-- Merged from file: THIRD_MANIFEST -->
+ <service android:name="com.google.android.apps.testapp3.TestService"/>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity-alias android:name="com.google.android.apps.testapp2.BarFoo"
+ android:targetActivity="com.google.android.apps.testapp2.FooBar"/>
+ </application>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <permission android:name="com.google.android.apps.foo.C2D_MESSAGE" android:protectionLevel="signature" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <uses-permission android:name="android.permission.INTERNET" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <!-- Comment for permission android.permission.GET_ACCOUNTS.
+ This is just to make sure the comment is being merged correctly.
+ -->
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+</manifest>
+"""
+
+
+ALIAS_MANIFEST = """<?xml version='1.0' encoding='utf-8'?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.apps.testapp"
+ android:versionCode="70"
+ android:versionName="1.0">
+ <uses-sdk android:minSdkVersion="10"/>
+ <uses-feature android:name="android.hardware.nfc" android:required="true" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <application
+ android:icon="@drawable/icon"
+ android:name="com.google.android.apps.testapp.TestApplication"
+ android:theme="@style/Theme.Test"
+ android:label="@string/app_name">
+ <activity-alias android:name="com.google.foo.should.not.be.first"
+ android:targetActivity=".entrypoint.EntryPointActivityGroup"/>
+ <!-- START LIBRARIES (Maintain Alphabetic order) -->
+ <!-- NFC extras -->
+ <uses-library android:name="com.google.android.nfc_extras" android:required="false"/>
+ <!-- END LIBRARIES -->
+ <!-- START ACTIVITIES (Maintain Alphabetic order) -->
+ <!-- Entry point activity - navigation and title bar. -->
+ <activity
+ android:name=".entrypoint.EntryPointActivityGroup"
+ android:screenOrientation="portrait"
+ android:launchMode="singleTop"/>
+ <activity android:name=".ui.topup.TopUpActivity" />
+ <service android:name=".nfcevent.NfcEventService" />
+ <receiver
+ android:name="com.receiver.TestReceiver"
+ android:process="@string/receiver_service_name">
+ <!-- Receive the actual message -->
+ <intent-filter>
+ <action
+ android:name="android.intent.action.USER_PRESENT"/>
+ </intent-filter>
+ </receiver>
+ <provider
+ android:name=".dataaccess.persistence.ContentProvider"
+ android:authorities="com.google.android.apps.testapp"
+ android:exported="false" />
+ </application>
+</manifest>
+"""
+
+
+# This case exists when a library manifest relies on
+# dependent manifests to provide required elements, i.e. a <application>
+INVALID_MERGER_MANIFEST = """
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.invalid"
+ android:versionCode="9100000"
+ android:versionName="9.1.0.0x">
+ <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
+</manifest>
+"""
+
+
+VALID_MANIFEST = """
+<manifest
+ android:versionCode="9100000"
+ android:versionName="9.1.0.0x"
+ package="com.google.android.invalid"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- *** WARNING *** DO NOT EDIT! THIS IS GENERATED MANIFEST BY MERGE_MANIFEST TOOL.
+ Merger manifest:
+ INVALID_MANIFEST
+ Mergee manifests:
+ SECOND_MANIFEST
+ -->
+ <application
+ android:backupAgent="com.google.android.apps.testapp2.FooBar"
+ android:icon="@drawable/icon"
+ android:label="@string/app_name"
+ android:name="com.google.android.apps.testapp.TestApplication2"
+ android:theme="@style/Theme.Test2">
+ <activity
+ android:name="com.google.android.apps.testapp2.TestActivity2"/>
+ <activity
+ android:name="com.google.android.apps.testapp2.PreviewActivity"/>
+ <activity android:excludeFromRecents="true"
+ android:name="com.google.android.apps.testapp2.ShowTextActivity"/>
+ <activity android:excludeFromRecents="true"
+ android:name="com.google.android.apps.testapp2.ShowStringListActivity"
+ android:parentActivityName="com.google.android.apps.testapp2.ui.home.HomeActivity">
+ </activity>
+ <service
+ android:name="com.google.android.apps.testapp2.TestService">
+ <meta-data android:name="param"
+ android:value="value"/>
+ </service>
+ <service
+ android:name="com.google.android.apps.testapp2.nfcevent.NfcEventService"/>
+ <receiver
+ android:enabled="false"
+ android:name="com.google.android.apps.testapp2.ConnectivityReceiver">
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+ </intent-filter>
+ </receiver>
+ <provider android:name="some.package.with.inner.class$AnInnerClass"/>
+ <provider
+ android:authorities="com.google.android.invalid.com.google.android.invalid"
+ android:exported="false" android:name="com.google.android.invalid"/>
+ <provider android:authorities="PlaceHolderProviderAuthorities.com.google.android.invalid"
+ android:exported="false"
+ android:name="com.google.android.invalid.PlaceHolderProviderName"/>
+ <activity android:name="activityPrefix.com.google.android.invalid.activitySuffix">
+ <intent-filter>
+ <action android:name="actionPrefix.com.google.android.invalid.actionSuffix"/>
+ </intent-filter>
+ </activity>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <activity android:label="@string/app_name"
+ android:name="com.google.android.apps.testapp2.ui.home.HomeActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity-alias
+ android:name="com.google.android.apps.testapp2.BarFoo"
+ android:targetActivity="com.google.android.apps.testapp2.FooBar"/>
+ </application>
+ <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21"/>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <permission android:name="com.google.android.apps.foo.C2D_MESSAGE" android:protectionLevel="signature"/>
+ <!-- Merged from file: SECOND_MANIFEST -->
+ <uses-feature android:name="android.hardware.nfc" android:required="true"/>
+</manifest>
+"""
+
+
+def Reformat(string):
+ """Reformat for comparison."""
+ string = re.compile(r'^[ \t]*\n?', re.MULTILINE).sub('', string)
+ return string
+
+
+class MergeManifestsTest(unittest.TestCase):
+ """Unit tests for the MergeManifest class."""
+
+ def testMerge(self):
+ self.maxDiff = None
+ merger = merge_manifests.MergeManifests(
+ (FIRST_MANIFEST, 'FIRST_MANIFEST'),
+ [(SECOND_MANIFEST, 'SECOND_MANIFEST'),
+ (THIRD_MANIFEST, 'THIRD_MANIFEST')],
+ ['android.permission.READ_LOGS'])
+ result = merger.Merge()
+ expected = xml.dom.minidom.parseString(MANUALLY_MERGED).toprettyxml()
+ self.assertEquals(Reformat(expected), Reformat(result))
+
+ def testReformat(self):
+ text = ' a\n b\n\n\n \t c'
+ expected = 'a\nb\nc'
+ self.assertEquals(expected, Reformat(text))
+
+ def testValidateAndWarnPermissions(self):
+ permissions = ['android.permission.VIBRATE', 'android.permission.LAUGH']
+ warnings = merge_manifests._ValidateAndWarnPermissions(permissions)
+ self.assertTrue('android.permission.VIBRATE' not in warnings)
+ self.assertTrue('android.permission.LAUGH' in warnings)
+
+ def testExcludeAllPermissions(self):
+ merger = merge_manifests.MergeManifests(
+ (FIRST_MANIFEST, 'FIRST_MANIFEST'),
+ [(SECOND_MANIFEST, 'SECOND_MANIFEST'),
+ (THIRD_MANIFEST, 'THIRD_MANIFEST')],
+ ['all'])
+ result = merger.Merge()
+ self.assertFalse('android.permission.READ_LOGS' in result)
+ self.assertFalse('android.permission.INTERNET' in result)
+ self.assertTrue('android.permission.ACCESS_COARSE_LOCATION' in result)
+
+ def testUndefinedArgumentPlaceholder(self):
+ bad_manifest = SECOND_MANIFEST.replace(
+ '${packageName}', '${unknownPlaceHolder}')
+ merger = merge_manifests.MergeManifests(
+ (FIRST_MANIFEST, 'FIRST_MANIFEST'),
+ [(bad_manifest, 'invalidManifest'),
+ (THIRD_MANIFEST, 'THIRD_MANIFEST')])
+ try:
+ merger.Merge()
+ self.fail('merging manifests with unknown placeholders didn\'t fail')
+ except merge_manifests.UndefinedPlaceholderException:
+ pass
+
+ def testActivityAliasesAreAlwaysLast(self):
+ merger = merge_manifests.MergeManifests(
+ (FIRST_MANIFEST, 'FIRST_MANIFEST'),
+ [(SECOND_MANIFEST, 'SECOND_MANIFEST'),
+ (ALIAS_MANIFEST, 'THIRD_MANIFEST')],
+ ['all'])
+ result = merger.Merge()
+ last_occurence_of_activity = result.rfind('<activity ')
+ first_occurence_of_alias = result.find('<activity-alias ')
+ self.assertLess(last_occurence_of_activity, first_occurence_of_alias,
+ msg='First activity-alias is not after the last activity!')
+
+ def testMergeToCreateValidManifest(self):
+ self.maxDiff = None
+ merger = merge_manifests.MergeManifests(
+ (INVALID_MERGER_MANIFEST, 'INVALID_MANIFEST'),
+ [(SECOND_MANIFEST, 'SECOND_MANIFEST')],
+ ['all'])
+ result = merger.Merge()
+ expected = xml.dom.minidom.parseString(VALID_MANIFEST).toprettyxml()
+ self.assertEquals(Reformat(expected), Reformat(result))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/tools/android/proguard_whitelister.py b/tools/android/proguard_whitelister.py
new file mode 100644
index 0000000000..69f213553c
--- /dev/null
+++ b/tools/android/proguard_whitelister.py
@@ -0,0 +1,73 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Checks for proguard configuration rules that cannot be combined across libs.
+
+The only valid proguard arguments for a library are -keep, -assumenosideeffects,
+and -dontnote and -dontwarn and -checkdiscard when they are provided with
+arguments.
+"""
+
+import re
+import sys
+
+from third_party.py import gflags
+
+gflags.DEFINE_string('path', None, 'Path to the proguard config to validate')
+gflags.DEFINE_string('output', None, 'Where to put the validated config')
+
+FLAGS = gflags.FLAGS
+PROGUARD_COMMENTS_PATTERN = '#.*(\n|$)'
+
+
+def main():
+ with open(FLAGS.path) as config:
+ config_string = config.read()
+ invalid_configs = Validate(config_string)
+ if invalid_configs:
+ raise RuntimeError('Invalid proguard config parameters: '
+ + str(invalid_configs))
+ with open(FLAGS.output, 'w+') as outconfig:
+ config_string = ('# Merged from %s \n' % FLAGS.path) + config_string
+ outconfig.write(config_string)
+
+
+def Validate(config):
+ """Checks the config for illegal arguments."""
+ config = re.sub(PROGUARD_COMMENTS_PATTERN, '', config)
+ args = config.split('-')
+ invalid_configs = []
+ for arg in args:
+ arg = arg.strip()
+ if not arg:
+ continue
+ elif arg.startswith('checkdiscard'):
+ continue
+ elif arg.startswith('keep'):
+ continue
+ elif arg.startswith('assumenosideeffects'):
+ continue
+ elif arg.split()[0] == 'dontnote':
+ if len(arg.split()) > 1:
+ continue
+ elif arg.split()[0] == 'dontwarn':
+ if len(arg.split()) > 1:
+ continue
+ invalid_configs.append('-' + arg.split()[0])
+
+ return invalid_configs
+
+if __name__ == '__main__':
+ FLAGS(sys.argv)
+ main()
diff --git a/tools/android/proguard_whitelister_input.cfg b/tools/android/proguard_whitelister_input.cfg
new file mode 100644
index 0000000000..9955226f80
--- /dev/null
+++ b/tools/android/proguard_whitelister_input.cfg
@@ -0,0 +1,48 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+# Note: put any environment specific flags in their respective proguard files:
+# proguard_test.flags, proguard_dev.flags, proguard_release.flags, etc
+
+# These classes reference resources that don't exist in pre-v11 builds
+-dontwarn com.google.android.apps.testapp.TestActivity
+
+# References to a hidden class
+-dontnote android.os.SystemProperties
+-keep class android.os.SystemProperties { *** get(...);}
+
+# Keep all classes extended from com.google.android.apps.testapp.MyBaseClass
+# because, e.g., we use reflection.
+-keep class * extends com.google.android.apps.testapp.MyBaseClass {
+ @com.google.android.apps.testapp.MyBaseClass$Inner <fields>;
+}
+
+# Needed because this field is accessed reflectively, and it exists in generated code.
+-keepclassmembers class * extends com.google.protobuf.nano.MessageNano {
+ *** apiHeader;
+}
+
+# Extensions are deserialized with a reflective call to newInstance().
+-keepclassmembers class * extends com.google.protobuf.nano.Extension {
+ <init>();
+}
+
+-dontnote android.support.v?.app.Fragment
+
+-keepnames class com.google.android.testapp.** extends com.google.android.testapp.resources.Resource { *; }
+
+-keepclasseswithmembers class derp.foo { bar;} -keepattributes *
+
+# This is a comment, so this should not cause problems -dontobfuscate
+# This is a comment, so # this should not cause problems -dontnote
diff --git a/tools/android/proguard_whitelister_test.py b/tools/android/proguard_whitelister_test.py
new file mode 100644
index 0000000000..05d1b02ed4
--- /dev/null
+++ b/tools/android/proguard_whitelister_test.py
@@ -0,0 +1,57 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+import os
+import unittest
+
+from tools.android import proguard_whitelister
+
+
+class ValidateProguardTest(unittest.TestCase):
+
+ def testValidConfig(self):
+ path = os.path.join(
+ os.path.dirname(__file__), "proguard_whitelister_input.cfg")
+ with open(path) as config:
+ self.assertEqual([], proguard_whitelister.Validate(config.read()))
+
+ def testInvalidNoteConfig(self):
+ self.assertEqual(["-dontnote"], proguard_whitelister.Validate(
+ """# We don't want libraries disabling notes globally.
+ -dontnote
+ """))
+
+ def testInvalidWarnConfig(self):
+ self.assertEqual(["-dontwarn"], proguard_whitelister.Validate(
+ """# We don't want libraries disabling warnings globally.
+ -dontwarn
+ """))
+
+ def testInvalidOptimizationConfig(self):
+ self.assertEqual(["-optimizations"], proguard_whitelister.Validate(
+ """#We don't want libraries disabling global optimizations.
+ -optimizations !class/merging/*,!code/allocation/variable
+ """))
+
+ def testMultipleInvalidArgs(self):
+ self.assertEqual(
+ ["-optimizations", "-dontnote"], proguard_whitelister.Validate(
+ """#We don't want libraries disabling global optimizations.
+ -optimizations !class/merging/*,!code/allocation/variable
+ -dontnote
+ """))
+
+
+if __name__ == "__main__":
+ unittest.main()