diff options
author | Alex Humesky <ahumesky@google.com> | 2015-05-26 22:23:13 +0000 |
---|---|---|
committer | Laurent Le Brun <laurentlb@google.com> | 2015-05-27 16:46:05 +0000 |
commit | b46c6351321c2950badb4c0e4071e3f20d49e338 (patch) | |
tree | 59710d4c8b959c1aa1ac093b1311e905745c03e9 /tools | |
parent | b2053d796acf4c1bc39ddc56769be22a9e09c0da (diff) |
Adds tools for building Android apps.
--
MOS_MIGRATED_REVID=94515805
Diffstat (limited to 'tools')
-rw-r--r-- | tools/android/BUILD | 43 | ||||
-rw-r--r-- | tools/android/android_permissions.py | 146 | ||||
-rw-r--r-- | tools/android/merge_manifests.py | 422 | ||||
-rw-r--r-- | tools/android/merge_manifests_test.py | 509 | ||||
-rw-r--r-- | tools/android/proguard_whitelister.py | 73 | ||||
-rw-r--r-- | tools/android/proguard_whitelister_input.cfg | 48 | ||||
-rw-r--r-- | tools/android/proguard_whitelister_test.py | 57 |
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() |