From 2eceaea745773bb654bb4e52c00cafdedc68c0ac Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Mon, 8 Feb 2010 16:48:01 -0800 Subject: [PATCH] APK checker. Compares the fields and methods accessed by an APK against the public API files generated by the build. See the README for details. --- tools/apkcheck/Android.mk | 42 +++ tools/apkcheck/README.txt | 39 ++ tools/apkcheck/etc/apkcheck | 46 +++ tools/apkcheck/etc/manifest.txt | 2 + tools/apkcheck/src/Android.mk | 28 ++ .../src/com/android/apkcheck/ApiDescrHandler.java | 147 ++++++++ .../apkcheck/src/com/android/apkcheck/ApiList.java | 104 ++++++ .../src/com/android/apkcheck/ApkCheck.java | 396 +++++++++++++++++++++ .../apkcheck/src/com/android/apkcheck/Builtin.java | 111 ++++++ .../src/com/android/apkcheck/ClassInfo.java | 316 ++++++++++++++++ .../src/com/android/apkcheck/FieldInfo.java | 64 ++++ .../src/com/android/apkcheck/MethodInfo.java | 95 +++++ .../src/com/android/apkcheck/PackageInfo.java | 79 ++++ .../src/com/android/apkcheck/TypeUtils.java | 253 +++++++++++++ 14 files changed, 1722 insertions(+) create mode 100644 tools/apkcheck/Android.mk create mode 100644 tools/apkcheck/README.txt create mode 100644 tools/apkcheck/etc/apkcheck create mode 100644 tools/apkcheck/etc/manifest.txt create mode 100644 tools/apkcheck/src/Android.mk create mode 100644 tools/apkcheck/src/com/android/apkcheck/ApiDescrHandler.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/ApiList.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/ApkCheck.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/Builtin.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/ClassInfo.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/FieldInfo.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/MethodInfo.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/PackageInfo.java create mode 100644 tools/apkcheck/src/com/android/apkcheck/TypeUtils.java diff --git a/tools/apkcheck/Android.mk b/tools/apkcheck/Android.mk new file mode 100644 index 00000000..c388939d --- /dev/null +++ b/tools/apkcheck/Android.mk @@ -0,0 +1,42 @@ +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +# We use copy-file-to-new-target so that the installed +# script file's timestamp is at least as new as the +# .jar file it wraps. + +# the execution script +# ============================================================ +include $(CLEAR_VARS) +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE := apkcheck + +include $(BUILD_SYSTEM)/base_rules.mk + +$(LOCAL_BUILT_MODULE): $(HOST_OUT_JAVA_LIBRARIES)/apkcheck$(COMMON_JAVA_PACKAGE_SUFFIX) +$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/etc/apkcheck | $(ACP) + @echo "Copy: $(PRIVATE_MODULE) ($@)" + $(copy-file-to-new-target) + $(hide) chmod 755 $@ + +# the other stuff +# ============================================================ +subdirs := $(addprefix $(LOCAL_PATH)/,$(addsuffix /Android.mk, \ + src \ + )) + +include $(subdirs) diff --git a/tools/apkcheck/README.txt b/tools/apkcheck/README.txt new file mode 100644 index 00000000..38e30d2f --- /dev/null +++ b/tools/apkcheck/README.txt @@ -0,0 +1,39 @@ +APK Checker + +This compares the set of classes, fields, and methods used by an Android +application against the published API. + +The public API description files live in the source tree, in +frameworks/base/api/. The dependency set for an APK can be generated with +"dexdeps". + +Use "apkcheck --help" to see a list of available options. + + +Due to limitations and omissions in the API description files, there may +be false-positives and false-negatives. When possible these are emitted +as warnings rather than errors. (You may need to specify "--warn" to +see them.) + +In some cases involving generic signatures it may not be possible to +accurately reconstruct the public API. Some popular cases have been +hard-coded into the program. They can be included by adding the following +to the command line: + + --uses-library=BUILTIN + +The "--uses-library" option allows you to specify additional API source +material. In the future this may be useful for applications that include +libraries with the "uses-library" directive. + + +Example use: + +% dexdeps out/target/product/sapphire/system/app/Gmail.apk > Gmail.apk.xml +% apkcheck --uses-library=BUILTIN frameworks/base/api/current.xml Gmail.apk.xml +Gmail.apk.xml: summary: 0 errors, 15 warnings + + +By using the numbered API files (1.xml, 2.xml) instead of current.xml you +can test the APK against a specific release. + diff --git a/tools/apkcheck/etc/apkcheck b/tools/apkcheck/etc/apkcheck new file mode 100644 index 00000000..78af93ad --- /dev/null +++ b/tools/apkcheck/etc/apkcheck @@ -0,0 +1,46 @@ +#!/bin/bash +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +libdir=`dirname $progdir`/framework + +javaOpts="" +while expr "x$1" : 'x-J' >/dev/null; do + opt=`expr "$1" : '-J\(.*\)'` + javaOpts="${javaOpts} -${opt}" + shift +done + +exec java $javaOpts -jar $libdir/apkcheck.jar "$@" diff --git a/tools/apkcheck/etc/manifest.txt b/tools/apkcheck/etc/manifest.txt new file mode 100644 index 00000000..aa4fef2f --- /dev/null +++ b/tools/apkcheck/etc/manifest.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Main-Class: com.android.apkcheck.ApkCheck diff --git a/tools/apkcheck/src/Android.mk b/tools/apkcheck/src/Android.mk new file mode 100644 index 00000000..abc813aa --- /dev/null +++ b/tools/apkcheck/src/Android.mk @@ -0,0 +1,28 @@ +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + + +# apkcheck java library +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_JAR_MANIFEST := ../etc/manifest.txt + +LOCAL_MODULE:= apkcheck + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/apkcheck/src/com/android/apkcheck/ApiDescrHandler.java b/tools/apkcheck/src/com/android/apkcheck/ApiDescrHandler.java new file mode 100644 index 00000000..427a20f4 --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/ApiDescrHandler.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import org.xml.sax.*; +import org.xml.sax.helpers.*; +import java.io.*; + + +/** + * Provides implementation for SAX parser. + */ +class ApiDescrHandler extends DefaultHandler { + /* + * Uber-container. + */ + private ApiList mApiList; + + /* + * Temporary objects, used as containers while we accumulate the + * innards. + */ + private PackageInfo mCurrentPackage = null; + private ClassInfo mCurrentClass = null; + private MethodInfo mCurrentMethod = null; + + /** + * Constructs an ApiDescrHandler. + * + * @param fileName Source file name, used for debugging. + */ + public ApiDescrHandler(ApiList apiList) { + mApiList = apiList; + } + + /** + * Returns the ApiList in its current state. Generally only + * makes sense to call here after parsing is completed. + */ + public ApiList getApiList() { + return mApiList; + } + + /** + * Processes start tags. If the file is malformed we will likely + * NPE, but this is captured by the caller. + * + * We currently assume that packages and classes only appear once, + * so all classes associated with a package are wrapped in a singular + * instance of <package>. We may want to remove this assumption + * by attempting to find an existing package/class with the same name. + */ + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) { + + if (qName.equals("package")) { + /* top-most element */ + mCurrentPackage = mApiList.getOrCreatePackage( + attributes.getValue("name")); + } else if (qName.equals("class") || qName.equals("interface")) { + /* get class, gather fields/methods and interfaces */ + mCurrentClass = mCurrentPackage.getOrCreateClass( + attributes.getValue("name"), + attributes.getValue("extends"), + attributes.getValue("static")); + } else if (qName.equals("implements")) { + /* add name of interface to current class */ + mCurrentClass.addInterface(attributes.getValue("name")); + } else if (qName.equals("method")) { + /* hold object while we gather parameters */ + mCurrentMethod = new MethodInfo(attributes.getValue("name"), + attributes.getValue("return")); + } else if (qName.equals("constructor")) { + /* like "method", but has no name or return type */ + mCurrentMethod = new MethodInfo("", "void"); + + /* + * If this is a non-static inner class, we want to add the + * "hidden" outer class parameter as the first parameter. + * We can tell if it's an inner class because the class name + * will include a '$' (it has been normalized already). + */ + String staticClass = mCurrentClass.getStatic(); + if (staticClass == null) { + /* + * We're parsing an APK file, which means we can't know + * if the class we're referencing is static or not. We + * also already have the "secret" first parameter + * represented in the method parameter list, so we don't + * need to insert it here. + */ + } else if ("false".equals(staticClass)) { + String className = mCurrentClass.getName(); + int dollarIndex = className.indexOf('$'); + if (dollarIndex >= 0) { + String outerClass = className.substring(0, dollarIndex); + //System.out.println("--- inserting " + + // mCurrentPackage.getName() + "." + outerClass + + // " into constructor for " + className); + mCurrentMethod.addParameter(mCurrentPackage.getName() + + "." + outerClass); + } + } + } else if (qName.equals("field")) { + /* add to current class */ + FieldInfo fInfo = new FieldInfo(attributes.getValue("name"), + attributes.getValue("type")); + mCurrentClass.addField(fInfo); + } else if (qName.equals("parameter")) { + /* add to current method */ + mCurrentMethod.addParameter(attributes.getValue("type")); + } + } + + /** + * Processes end tags. Generally these add the under-construction + * item to the appropriate container. + */ + @Override + public void endElement(String uri, String localName, String qName) { + if (qName.equals("method") || qName.equals("constructor")) { + /* add method to class */ + mCurrentClass.addMethod(mCurrentMethod); + mCurrentMethod = null; + } else if (qName.equals("class") || qName.equals("interface")) { + mCurrentClass = null; + } else if (qName.equals("package")) { + mCurrentPackage = null; + } + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/ApiList.java b/tools/apkcheck/src/com/android/apkcheck/ApiList.java new file mode 100644 index 00000000..9eb70017 --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/ApiList.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * Holds a list of API members, including classes, fields, and methods. + */ +public class ApiList { + private HashMap mPackageList; + private String mDebugString; + private int mWarnings, mErrors; + + /** + * Constructs an ApiList. + * + * @param debugString Identification string useful for debugging. + */ + public ApiList(String debugString) { + mPackageList = new HashMap(); + mDebugString = debugString; + } + + /** + * Returns the source filename. Useful for debug messages only. + */ + public String getDebugString() { + return mDebugString; + } + + /** + * Increment the number of warnings associated with this API list. + */ + public void incrWarnings() { + mWarnings++; + } + + /** + * Increment the errors of warnings associated with this API list. + */ + public void incrErrors() { + mErrors++; + } + + /** + * Returns the number of warnings associated with this API list. + */ + public int getWarningCount() { + return mWarnings; + } + + /** + * Returns the number of errors associated with this API list. + */ + public int getErrorCount() { + return mErrors; + } + + /** + * Retrieves the named package. + * + * @return the package, or null if no match was found + */ + public PackageInfo getPackage(String name) { + return mPackageList.get(name); + } + + /** + * Retrieves the named package, creating it if it doesn't already + * exist. + */ + public PackageInfo getOrCreatePackage(String name) { + PackageInfo pkgInfo = mPackageList.get(name); + if (pkgInfo == null) { + pkgInfo = new PackageInfo(name); + mPackageList.put(name, pkgInfo); + } + return pkgInfo; + } + + /** + * Returns an iterator for the set of known packages. + */ + public Iterator getPackageIterator() { + return mPackageList.values().iterator(); + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/ApkCheck.java b/tools/apkcheck/src/com/android/apkcheck/ApkCheck.java new file mode 100644 index 00000000..42196a54 --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/ApkCheck.java @@ -0,0 +1,396 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import org.xml.sax.*; +import org.xml.sax.helpers.*; +import java.io.*; +import java.util.ArrayList; +import java.util.Iterator; + + +/** + * Checks an APK's dependencies against the published API specification. + * + * We need to read two XML files (spec and APK) and perform some operations + * on the elements. The file formats are similar but not identical, so + * we distill it down to common elements. + * + * We may also want to read some additional API lists representing + * libraries that would be included with a "uses-library" directive. + * + * For performance we want to allow processing of multiple APKs so + * we don't have to re-parse the spec file each time. + */ +public class ApkCheck { + /* keep track of current APK file name, for error messages */ + private static ApiList sCurrentApk; + + /* show warnings? */ + private static boolean sShowWarnings = false; + /* show errors? */ + private static boolean sShowErrors = true; + + /** + * Program entry point. + */ + public static void main(String[] args) { + ApiList apiDescr = new ApiList("public-api"); + + if (args.length < 2) { + usage(); + return; + } + + /* process args */ + int idx; + for (idx = 0; idx < args.length; idx++) { + if (args[idx].equals("--help")) { + usage(); + return; + } else if (args[idx].startsWith("--uses-library=")) { + String libName = args[idx].substring(args[idx].indexOf('=')+1); + if ("BUILTIN".equals(libName)) { + Reader reader = Builtin.getReader(); + if (!parseXml(apiDescr, reader, "BUILTIN")) + return; + } else { + if (!parseApiDescr(apiDescr, libName)) + return; + } + } else if (args[idx].equals("--warn")) { + sShowWarnings = true; + } else if (args[idx].equals("--no-warn")) { + sShowWarnings = false; + } else if (args[idx].equals("--error")) { + sShowErrors = true; + } else if (args[idx].equals("--no-error")) { + sShowErrors = false; + + } else if (args[idx].startsWith("--")) { + if (args[idx].equals("--")) { + // remainder are filenames, even if they start with "--" + idx++; + break; + } else { + // unknown option specified + System.err.println("ERROR: unknown option " + + args[idx] + " (use \"--help\" for usage info)"); + return; + } + } else { + break; + } + } + if (idx > args.length - 2) { + usage(); + return; + } + + /* parse base API description */ + if (!parseApiDescr(apiDescr, args[idx++])) + return; + + /* "flatten" superclasses and interfaces */ + sCurrentApk = apiDescr; + flattenInherited(apiDescr); + + /* walk through list of libs we want to scan */ + for ( ; idx < args.length; idx++) { + ApiList apkDescr = new ApiList(args[idx]); + sCurrentApk = apkDescr; + boolean success = parseApiDescr(apkDescr, args[idx]); + if (!success) { + if (idx < args.length-1) + System.err.println("Skipping..."); + continue; + } + + check(apiDescr, apkDescr); + System.out.println(args[idx] + ": summary: " + + apkDescr.getErrorCount() + " errors, " + + apkDescr.getWarningCount() + " warnings\n"); + } + } + + /** + * Prints usage statement. + */ + static void usage() { + System.err.println("Android APK checker v1.0"); + System.err.println("Copyright (C) 2010 The Android Open Source Project\n"); + System.err.println("Usage: apkcheck [options] public-api.xml apk1.xml ...\n"); + System.err.println("Options:"); + System.err.println(" --help show this message"); + System.err.println(" --uses-library=lib.xml load additional public API list"); + System.err.println(" --[no-]warn enable or disable display of warnings"); + System.err.println(" --[no-]error enable or disable display of errors"); + } + + /** + * Opens the file and passes it to parseXml. + * + * TODO: allow '-' as an alias for stdin? + */ + static boolean parseApiDescr(ApiList apiList, String fileName) { + boolean result = false; + + try { + FileReader fileReader = new FileReader(fileName); + result = parseXml(apiList, fileReader, fileName); + fileReader.close(); + } catch (IOException ioe) { + System.err.println("Error opening " + fileName); + } + return result; + } + + /** + * Parses an XML file holding an API description. + * + * @param fileReader Data source. + * @param apiList Container to add stuff to. + * @param fileName Input file name, only used for debug messages. + */ + static boolean parseXml(ApiList apiList, Reader reader, + String fileName) { + //System.out.println("--- parsing " + fileName); + try { + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + ApiDescrHandler handler = new ApiDescrHandler(apiList); + xmlReader.setContentHandler(handler); + xmlReader.setErrorHandler(handler); + xmlReader.parse(new InputSource(reader)); + + //System.out.println("--- parsing complete"); + //dumpApi(apiList); + return true; + } catch (SAXParseException ex) { + System.err.println("Error parsing " + fileName + " line " + + ex.getLineNumber() + ": " + ex.getMessage()); + } catch (Exception ex) { + System.err.println("Error while reading " + fileName + ": " + + ex.getMessage()); + ex.printStackTrace(); + } + + // failed + return false; + } + + /** + * Expands lists of fields and methods to recursively include superclass + * and interface entries. + * + * The API description files have entries for every method a class + * declares, even if it's present in the superclass (e.g. toString()). + * Removal of one of these methods doesn't constitute an API change, + * though, so if we don't find a method in a class we need to hunt + * through its superclasses. + * + * We can walk up the hierarchy while analyzing the target APK, + * or we can "flatten" the methods declared by the superclasses and + * interfaces before we begin the analysis. Expanding up front can be + * beneficial if we're analyzing lots of APKs in one go, but detrimental + * to startup time if we just want to look at one small APK. + * + * It also means filling the field/method hash tables with lots of + * entries that never get used, possibly worsening the hash table + * hit rate. + * + * We only need to do this for the public API list. The dexdeps output + * doesn't have this sort of information anyway. + */ + static void flattenInherited(ApiList pubList) { + Iterator pkgIter = pubList.getPackageIterator(); + while (pkgIter.hasNext()) { + PackageInfo pubPkgInfo = pkgIter.next(); + + Iterator classIter = pubPkgInfo.getClassIterator(); + while (classIter.hasNext()) { + ClassInfo pubClassInfo = classIter.next(); + + pubClassInfo.flattenClass(pubList); + } + } + } + + /** + * Checks the APK against the public API. + * + * Run through and find the mismatches. + * + * @return true if all is well + */ + static boolean check(ApiList pubList, ApiList apkDescr) { + + Iterator pkgIter = apkDescr.getPackageIterator(); + while (pkgIter.hasNext()) { + PackageInfo apkPkgInfo = pkgIter.next(); + PackageInfo pubPkgInfo = pubList.getPackage(apkPkgInfo.getName()); + boolean badPackage = false; + + if (pubPkgInfo == null) { + // "illegal package" not a tremendously useful message + //apkError("Illegal package ref: " + apkPkgInfo.getName()); + badPackage = true; + } + + Iterator classIter = apkPkgInfo.getClassIterator(); + while (classIter.hasNext()) { + ClassInfo apkClassInfo = classIter.next(); + + if (badPackage) { + /* list the offending classes */ + apkError("Illegal class ref: " + + apkPkgInfo.getName() + "." + apkClassInfo.getName()); + } else { + checkClass(pubPkgInfo, apkClassInfo); + } + } + } + + return true; + } + + /** + * Checks the class against the public API. We check the class + * itself and then any fields and methods. + */ + static boolean checkClass(PackageInfo pubPkgInfo, ClassInfo classInfo) { + + ClassInfo pubClassInfo = pubPkgInfo.getClass(classInfo.getName()); + + if (pubClassInfo == null) { + if (classInfo.hasNoFieldMethod()) { + apkWarning("Hidden class referenced: " + + pubPkgInfo.getName() + "." + classInfo.getName()); + } else { + apkError("Illegal class ref: " + + pubPkgInfo.getName() + "." + classInfo.getName()); + // could list specific fields/methods used + } + return false; + } + + /* + * Check the contents of classInfo against pubClassInfo. + */ + Iterator fieldIter = classInfo.getFieldIterator(); + while (fieldIter.hasNext()) { + FieldInfo apkFieldInfo = fieldIter.next(); + String nameAndType = apkFieldInfo.getNameAndType(); + FieldInfo pubFieldInfo = pubClassInfo.getField(nameAndType); + if (pubFieldInfo == null) { + if ("java.lang.Enum".equals(pubClassInfo.getSuperclassName())) { + apkWarning("Enum field ref: " + pubPkgInfo.getName() + + "." + classInfo.getName() + "." + nameAndType); + } else { + apkError("Illegal field ref: " + pubPkgInfo.getName() + + "." + classInfo.getName() + "." + nameAndType); + } + } + } + + Iterator methodIter = classInfo.getMethodIterator(); + while (methodIter.hasNext()) { + MethodInfo apkMethodInfo = methodIter.next(); + String nameAndDescr = apkMethodInfo.getNameAndDescriptor(); + MethodInfo pubMethodInfo = pubClassInfo.getMethod(nameAndDescr); + if (pubMethodInfo == null) { + pubMethodInfo = pubClassInfo.getMethodIgnoringReturn(nameAndDescr); + if (pubMethodInfo == null) { + apkError("Illegal method ref: " + pubPkgInfo.getName() + + "." + classInfo.getName() + "." + nameAndDescr); + } else { + apkWarning("Possibly covariant method ref: " + + pubPkgInfo.getName() + "." + classInfo.getName() + + "." + nameAndDescr); + } + } + } + + + return true; + } + + + /** + * Prints a warning message about an APK problem. + */ + public static void apkWarning(String msg) { + if (sShowWarnings) { + System.out.println("(warn) " + sCurrentApk.getDebugString() + + ": " + msg); + } + sCurrentApk.incrWarnings(); + } + + /** + * Prints an error message about an APK problem. + */ + public static void apkError(String msg) { + if (sShowErrors) { + System.out.println(sCurrentApk.getDebugString() + ": " + msg); + } + sCurrentApk.incrErrors(); + } + + /** + * Recursively dumps the contents of the API. Sort order is not + * specified. + */ + private static void dumpApi(ApiList apiList) { + Iterator iter = apiList.getPackageIterator(); + while (iter.hasNext()) { + PackageInfo pkgInfo = iter.next(); + dumpPackage(pkgInfo); + } + } + + private static void dumpPackage(PackageInfo pkgInfo) { + Iterator iter = pkgInfo.getClassIterator(); + System.out.println("PACKAGE " + pkgInfo.getName()); + while (iter.hasNext()) { + ClassInfo classInfo = iter.next(); + dumpClass(classInfo); + } + } + + private static void dumpClass(ClassInfo classInfo) { + System.out.println(" CLASS " + classInfo.getName()); + Iterator fieldIter = classInfo.getFieldIterator(); + while (fieldIter.hasNext()) { + FieldInfo fieldInfo = fieldIter.next(); + dumpField(fieldInfo); + } + Iterator methIter = classInfo.getMethodIterator(); + while (methIter.hasNext()) { + MethodInfo methInfo = methIter.next(); + dumpMethod(methInfo); + } + } + + private static void dumpMethod(MethodInfo methInfo) { + System.out.println(" METHOD " + methInfo.getNameAndDescriptor()); + } + + private static void dumpField(FieldInfo fieldInfo) { + System.out.println(" FIELD " + fieldInfo.getNameAndType()); + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/Builtin.java b/tools/apkcheck/src/com/android/apkcheck/Builtin.java new file mode 100644 index 00000000..8e3b196c --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/Builtin.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.io.StringReader; + +/** + * Class containing "built-in" API description entries. + * + * There are some bugs in the API description file that we can't work around + * (notably some ambiguity with generic types). The easiest way to cope + * is to supply the correct definitions in an add-on file. Rather than + * cart around an extra file, we bake them in here. + */ +public class Builtin { + private Builtin() {} + + private static final String BUILTIN = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + "\n" + ; + + /** + * Returns the built-in definition "file". + */ + public static StringReader getReader() { + return new StringReader(BUILTIN); + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/ClassInfo.java b/tools/apkcheck/src/com/android/apkcheck/ClassInfo.java new file mode 100644 index 00000000..6ceb5316 --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/ClassInfo.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Container representing a class or interface with fields and methods. + */ +public class ClassInfo { + private String mName; + // methods are hashed on name:descriptor + private HashMap mMethodList; + // fields are hashed on name:type + private HashMap mFieldList; + + private String mSuperclassName; + + // is this a static inner class? + private String mIsStatic; + + // holds the name of the superclass and all declared interfaces + private ArrayList mSuperNames; + + private boolean mFlattening = false; + private boolean mFlattened = false; + + /** + * Constructs a new ClassInfo with the provided class name. + * + * @param className Binary class name without the package name, + * e.g. "AlertDialog$Builder". + * @param superclassName Fully-qualified binary or non-binary superclass + * name (e.g. "java.lang.Enum"). + * @param isStatic Class static attribute, may be "true", "false", or null. + */ + public ClassInfo(String className, String superclassName, String isStatic) { + mName = className; + mMethodList = new HashMap(); + mFieldList = new HashMap(); + mSuperNames = new ArrayList(); + mIsStatic = isStatic; + + /* + * Record the superclass name, and add it to the interface list + * since we'll need to do the same "flattening" work on it. + * + * Interfaces and java.lang.Object have a null value. + */ + if (superclassName != null) { + mSuperclassName = superclassName; + mSuperNames.add(superclassName); + } + } + + /** + * Returns the name of the class. + */ + public String getName() { + return mName; + } + + /** + * Returns the name of the superclass. + */ + public String getSuperclassName() { + return mSuperclassName; + } + + /** + * Returns the "static" attribute. + * + * This is actually tri-state: + * "true" means it is static + * "false" means it's not static + * null means it's unknown + * + * The "unknown" state is associated with the APK input, while the + * known states are from the public API definition. + * + * This relates to the handling of the "secret" first parameter to + * constructors of non-static inner classes. + */ + public String getStatic() { + return mIsStatic; + } + + /** + * Adds a field to the list. + */ + public void addField(FieldInfo fieldInfo) { + mFieldList.put(fieldInfo.getNameAndType(), fieldInfo); + } + + /** + * Retrives a field from the list. + * + * @param nameAndType fieldName:type + */ + public FieldInfo getField(String nameAndType) { + return mFieldList.get(nameAndType); + } + + /** + * Returns an iterator over all known fields. + */ + public Iterator getFieldIterator() { + return mFieldList.values().iterator(); + } + + /** + * Adds a method to the list. + */ + public void addMethod(MethodInfo methInfo) { + mMethodList.put(methInfo.getNameAndDescriptor(), methInfo); + } + + /** + * Returns an iterator over all known methods. + */ + public Iterator getMethodIterator() { + return mMethodList.values().iterator(); + } + + /** + * Retrieves a method from the list. + * + * @param nameAndDescr methodName:descriptor + */ + public MethodInfo getMethod(String nameAndDescr) { + return mMethodList.get(nameAndDescr); + } + + /** + * Retrieves a method from the list, matching on the part of the key + * before the return type. + * + * The API file doesn't include an entry for a method that overrides + * a method in the superclass. Ordinarily this is a good thing, but + * if the override uses a covariant return type then the reference + * to it in the APK won't match. + * + * @param nameAndDescr methodName:descriptor + */ + public MethodInfo getMethodIgnoringReturn(String nameAndDescr) { + String shortKey = nameAndDescr.substring(0, nameAndDescr.indexOf(')')+1); + + Iterator iter = getMethodIterator(); + while (iter.hasNext()) { + MethodInfo methInfo = iter.next(); + String nad = methInfo.getNameAndDescriptor(); + if (nad.startsWith(shortKey)) + return methInfo; + } + + return null; + } + + /** + * Returns true if the method and field lists are empty. + */ + public boolean hasNoFieldMethod() { + return mMethodList.size() == 0 && mFieldList.size() == 0; + } + + /** + * Adds an interface to the list of classes implemented by this class. + */ + public void addInterface(String interfaceName) { + mSuperNames.add(interfaceName); + } + + /** + * Flattens a class. This involves copying all methods and fields + * declared by the superclass and interfaces (and, recursively, their + * superclasses and interfaces) into the local structure. + * + * The public API file must be fully parsed before calling here. + */ + public void flattenClass(ApiList apiList) { + if (mFlattened) + return; + + /* + * Recursive class definitions aren't allowed in Java code, but + * there could be one in the API definition file. + */ + if (mFlattening) { + throw new RuntimeException("Recursive invoke; current class is " + + mName); + } + mFlattening = true; + + /* + * Normalize the ambiguous types. This requires regenerating the + * field and method lists, because the signature is used as the + * hash table key. + */ + normalizeTypes(apiList); + + /* + * Flatten our superclass and interfaces. + */ + for (int i = 0; i < mSuperNames.size(); i++) { + /* + * The contents of mSuperNames are in an ambiguous form. + * Normalize it to binary form before working with it. + */ + String interfaceName = TypeUtils.ambiguousToBinaryName(mSuperNames.get(i), + apiList); + ClassInfo classInfo = lookupClass(interfaceName, apiList); + if (classInfo == null) { + ApkCheck.apkWarning("Class " + interfaceName + + " not found (super of " + mName + ")"); + continue; + } + + /* flatten it */ + classInfo.flattenClass(apiList); + + /* copy everything from it in here */ + mergeFrom(classInfo); + } + + mFlattened = true; + } + + /** + * Normalizes the type names used in field and method descriptors. + * + * We call the field/method normalization function, which updates how + * it thinks of itself (and may be called multiple times from different + * classes). We then have to re-add it to the hash map because the + * key may have changed. (We're using an iterator, so we create a + * new hashmap and replace the old.) + */ + private void normalizeTypes(ApiList apiList) { + Iterator keyIter; + + HashMap tmpFieldList = new HashMap(); + keyIter = mFieldList.keySet().iterator(); + while (keyIter.hasNext()) { + String key = keyIter.next(); + FieldInfo fieldInfo = mFieldList.get(key); + fieldInfo.normalizeType(apiList); + tmpFieldList.put(fieldInfo.getNameAndType(), fieldInfo); + } + mFieldList = tmpFieldList; + + HashMap tmpMethodList = new HashMap(); + keyIter = mMethodList.keySet().iterator(); + while (keyIter.hasNext()) { + String key = keyIter.next(); + MethodInfo methodInfo = mMethodList.get(key); + methodInfo.normalizeTypes(apiList); + tmpMethodList.put(methodInfo.getNameAndDescriptor(), methodInfo); + } + mMethodList = tmpMethodList; + } + + /** + * Merges the fields and methods from "otherClass" into this class. + * + * Redundant entries will be merged. We don't specify who the winner + * will be. + */ + private void mergeFrom(ClassInfo otherClass) { + /*System.out.println("merging into " + getName() + ": fields=" + + mFieldList.size() + "/" + otherClass.mFieldList.size() + + ", methods=" + + mMethodList.size() + "/" + otherClass.mMethodList.size());*/ + + mFieldList.putAll(otherClass.mFieldList); + mMethodList.putAll(otherClass.mMethodList); + + /*System.out.println(" now fields=" + mFieldList.size() + + ", methods=" + mMethodList.size());*/ + } + + + /** + * Finds the named class in the ApiList. + * + * @param className Fully-qualified dot notation (e.g. "java.lang.String") + * @param apiList The hierarchy to search in. + * @return The class or null if not found. + */ + private static ClassInfo lookupClass(String fullname, ApiList apiList) { + String packageName = TypeUtils.packageNameOnly(fullname); + String className = TypeUtils.classNameOnly(fullname); + + PackageInfo pkg = apiList.getPackage(packageName); + if (pkg == null) + return null; + return pkg.getClass(className); + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/FieldInfo.java b/tools/apkcheck/src/com/android/apkcheck/FieldInfo.java new file mode 100644 index 00000000..4ab0665a --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/FieldInfo.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +/** + * Container representing a method with parameters. + */ +public class FieldInfo { + private String mName; + private String mType; + private String mNameAndType; + private boolean mTypeNormalized; + + /** + * Constructs a FieldInfo. + * + * @param name Field name. + * @param type Fully-qualified binary or non-binary type name. + */ + public FieldInfo(String name, String type) { + mName = name; + mType = type; + } + + /** + * Returns the combined name and type. This value is used as a hash + * table key. + */ + public String getNameAndType() { + if (mNameAndType == null) + mNameAndType = mName + ":" + TypeUtils.typeToDescriptor(mType); + return mNameAndType; + } + + /** + * Normalize the type used in fields. + */ + public void normalizeType(ApiList apiList) { + if (!mTypeNormalized) { + String type = TypeUtils.ambiguousToBinaryName(mType, apiList); + if (!type.equals(mType)) { + /* name changed, force regen on name+type */ + mType = type; + mNameAndType = null; + } + mTypeNormalized = true; + } + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/MethodInfo.java b/tools/apkcheck/src/com/android/apkcheck/MethodInfo.java new file mode 100644 index 00000000..79e4cd5a --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/MethodInfo.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Container representing a method with parameters. + */ +public class MethodInfo { + private String mName; + private String mReturn; + private String mNameAndDescriptor; + private ArrayList mParameters; + private boolean mParametersNormalized; + + /** + * Constructs MethodInfo. Tuck the method return type away for + * later construction of the signature. + */ + public MethodInfo(String name, String returnType) { + mName = name; + mReturn = returnType; + mParameters = new ArrayList(); + } + + /** + * Returns the method signature. This is generated when needed. + */ + public String getNameAndDescriptor() { + if (mNameAndDescriptor == null) { + StringBuilder newSig = new StringBuilder(mName); + newSig.append(":("); + for (int i = 0; i < mParameters.size(); i++) { + String humanType = mParameters.get(i); + String sigType = TypeUtils.typeToDescriptor(humanType); + newSig.append(sigType); + } + newSig.append(")"); + newSig.append(TypeUtils.typeToDescriptor(mReturn)); + mNameAndDescriptor = newSig.toString(); + } + return mNameAndDescriptor; + } + + /** + * Adds a parameter to the method. The "type" is a primitive or + * object type, formatted in human-centric form. For now we just + * store it. + */ + public void addParameter(String type) { + mParameters.add(type); + if (mNameAndDescriptor != null) { + System.err.println("WARNING: late add of params to method"); + mNameAndDescriptor = null; // force regen + } + } + + /** + * Normalizes the types in parameter lists to unambiguous binary form. + * + * The public API file must be fully parsed before calling here, + * because we need the full set of package names. + */ + public void normalizeTypes(ApiList apiList) { + if (!mParametersNormalized) { + mReturn = TypeUtils.ambiguousToBinaryName(mReturn, apiList); + + for (int i = 0; i < mParameters.size(); i++) { + String fixed = TypeUtils.ambiguousToBinaryName(mParameters.get(i), + apiList); + mParameters.set(i, fixed); + } + + mNameAndDescriptor = null; // force regen + mParametersNormalized = true; + } + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/PackageInfo.java b/tools/apkcheck/src/com/android/apkcheck/PackageInfo.java new file mode 100644 index 00000000..533f9176 --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/PackageInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * Container representing a package of classes and interfaces. + */ +public class PackageInfo { + private String mName; + private HashMap mClassList; + + public PackageInfo(String name) { + mName = name; + mClassList = new HashMap(); + } + + public String getName() { + return mName; + } + + /** + * Retrieves the named class. + * + * @return the package, or null if no match was found + */ + public ClassInfo getClass(String name) { + return mClassList.get(name); + } + + /** + * Retrieves the named class, creating it if it doesn't already + * exist. + * + * @param className Binary or non-binary class name without the + * package name, e.g. "AlertDialog.Builder". + * @param superclassName Fully-qualified binary or non-binary superclass + * name (e.g. "java.lang.Enum"). + * @param isStatic Class static attribute, may be "true", "false", or null. + */ + public ClassInfo getOrCreateClass(String className, String superclassName, + String isStatic) { + String fixedName = TypeUtils.simpleClassNameToBinary(className); + ClassInfo classInfo = mClassList.get(fixedName); + if (classInfo == null) { + //System.out.println("--- creating entry for class " + fixedName + + // " (super=" + superclassName + ")"); + classInfo = new ClassInfo(fixedName, superclassName, isStatic); + mClassList.put(fixedName, classInfo); + } else { + //System.out.println("--- returning existing class " + name); + } + return classInfo; + } + + /** + * Returns an iterator for the set of classes in this package. + */ + public Iterator getClassIterator() { + return mClassList.values().iterator(); + } +} + diff --git a/tools/apkcheck/src/com/android/apkcheck/TypeUtils.java b/tools/apkcheck/src/com/android/apkcheck/TypeUtils.java new file mode 100644 index 00000000..75e2bcdf --- /dev/null +++ b/tools/apkcheck/src/com/android/apkcheck/TypeUtils.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apkcheck; + +import java.util.HashMap; + +public class TypeUtils { + private void TypeUtils() {} + + /* + * Conversions for the primitive types, as well as a few things + * that show up a lot so we can avoid the string manipulation. + */ + private static final HashMap sQuickConvert; + static { + sQuickConvert = new HashMap(); + + sQuickConvert.put("boolean", "Z"); + sQuickConvert.put("byte", "B"); + sQuickConvert.put("char", "C"); + sQuickConvert.put("short", "S"); + sQuickConvert.put("int", "I"); + sQuickConvert.put("float", "F"); + sQuickConvert.put("long", "J"); + sQuickConvert.put("double", "D"); + sQuickConvert.put("void", "V"); + sQuickConvert.put("java.lang.Object", "Ljava/lang/Object;"); + sQuickConvert.put("java.lang.String", "Ljava/lang/String;"); + sQuickConvert.put("java.util.ArrayList", "Ljava/util/ArrayList;"); + sQuickConvert.put("java.util.HashMap", "Ljava/util/HashMap;"); + }; + + /* + * Convert a human-centric type into something suitable for a method + * signature. Examples: + * + * int --> I + * float[] --> [F + * java.lang.String --> Ljava/lang/String; + */ + public static String typeToDescriptor(String type) { + String quick = sQuickConvert.get(type); + if (quick != null) + return quick; + + int arrayDepth = 0; + int firstPosn = -1; + int posn = -1; + while ((posn = type.indexOf('[', posn+1)) != -1) { + if (firstPosn == -1) + firstPosn = posn; + arrayDepth++; + } + + /* if we found an array, strip the brackets off */ + if (firstPosn != -1) + type = type.substring(0, firstPosn); + + StringBuilder builder = new StringBuilder(); + while (arrayDepth-- > 0) + builder.append("["); + + /* retry quick convert */ + quick = sQuickConvert.get(type); + if (quick != null) { + builder.append(quick); + } else { + builder.append("L"); + builder.append(type.replace('.', '/')); + builder.append(";"); + } + + return builder.toString(); + } + + /** + * Converts a "simple" class name into a "binary" class name. For + * example: + * + * SharedPreferences.Editor --> SharedPreferences$Editor + * + * Do not use this on fully-qualified class names. + */ + public static String simpleClassNameToBinary(String className) { + return className.replace('.', '$'); + } + + /** + * Returns the class name portion of a fully-qualified binary class name. + */ + static String classNameOnly(String typeName) { + int start = typeName.lastIndexOf("."); + if (start < 0) { + return typeName; + } else { + return typeName.substring(start+1); + } + } + + /** + * Returns the package portion of a fully-qualified binary class name. + */ + static String packageNameOnly(String typeName) { + int end = typeName.lastIndexOf("."); + if (end < 0) { + /* lives in default package */ + return ""; + } else { + return typeName.substring(0, end); + } + } + + + /** + * Normalizes a full class name to binary form. + * + * For example, "android.view.View.OnClickListener" could be in + * the "android.view" package or the "android.view.View" package. + * Checking capitalization is unreliable. We do have a full list + * of package names from the file though, so there's an excellent + * chance that we can identify the package that way. (Of course, we + * can only do this after we have finished parsing the file.) + * + * If the name has two or more dots, we need to compare successively + * shorter strings until we find a match in the package list. + * + * Do not call this on previously-returned output, as that may + * confuse the code that deals with generic signatures. + */ + public static String ambiguousToBinaryName(String typeName, ApiList apiList) { + /* + * In some cases this can be a generic signature: + * + * + * + * If we see a '<', truncate the string at that point. That does + * pretty much the right thing. + * + * Handling the second item is ugly. If the string is a single + * character, change it to java.lang.Object. This is generally + * insufficient and also ambiguous with respect to classes in the + * default package, but we don't have much choice here, and it gets + * us through the standard collection classes. Note this is risky + * if somebody tries to normalize a string twice, since we could be + * "promoting" a primitive type. + */ + int ltOffset = typeName.indexOf('<'); + if (ltOffset >= 0) { + //System.out.println("stripping: " + typeName); + typeName = typeName.substring(0, ltOffset); + } + if (typeName.length() == 1) { + //System.out.println("converting X to Object: " + typeName); + typeName = "java.lang.Object"; + } else if (typeName.length() == 3 && + typeName.substring(1, 3).equals("[]")) { + //System.out.println("converting X[] to Object[]: " + typeName); + typeName = "java.lang.Object[]"; + } else if (typeName.length() == 4 && + typeName.substring(1, 4).equals("...")) { + //System.out.println("converting X... to Object[]: " + typeName); + typeName = "java.lang.Object[]"; + } + + /* + * Catch-all for varargs, which come in different varieties: + * java.lang.Object... + * java.lang.Class... + * java.lang.CharSequence... + * int... + * Progress... + * + * The latter is a generic type that we didn't catch above because + * it's not using a single-character descriptor. + * + * The method reference for "java.lang.Class..." will be looking + * for java.lang.Class[], not java.lang.Object[], so we don't want + * to do a blanket conversion. Similarly, "int..." turns into int[]. + * + * There's not much we can do with "Progress...", unless we want + * to write off the default package and filter out primitive types. + * Probably easier to fix it up elsewhere. + */ + int ellipsisIndex = typeName.indexOf("..."); + if (ellipsisIndex >= 0) { + String newTypeName = typeName.substring(0, ellipsisIndex) + "[]"; + //System.out.println("vararg " + typeName + " --> " + newTypeName); + typeName = newTypeName; + } + + /* + * It's possible the code that generates API definition files + * has been fixed. If we see a '$', just return the original. + */ + if (typeName.indexOf('$') >= 0) + return typeName; + + int lastDot = typeName.lastIndexOf('.'); + if (lastDot < 0) + return typeName; + + /* + * What we have looks like some variation of these: + * package.Class + * Class.InnerClass + * long.package.name.Class + * long.package.name.Class.InnerClass + * + * We cut it off at the last '.' and test to see if it's a known + * package name. If not, keep moving left until we run out of dots. + */ + int nextDot = lastDot; + while (nextDot >= 0) { + String testName = typeName.substring(0, nextDot); + if (apiList.getPackage(testName) != null) { + break; + } + + nextDot = typeName.lastIndexOf('.', nextDot-1); + } + + if (nextDot < 0) { + /* no package name found, convert all dots */ + System.out.println("+++ no pkg name found on " + typeName + typeName.length()); + typeName = typeName.replace('.', '$'); + } else if (nextDot == lastDot) { + /* class name is last element; original string is fine */ + } else { + /* in the middle; zap the dots in the inner class name */ + String oldClassName = typeName; + typeName = typeName.substring(0, nextDot+1) + + typeName.substring(nextDot+1).replace('.', '$'); + //System.out.println("+++ " + oldClassName + " --> " + typeName); + } + + return typeName; + } +} + -- 2.11.0