--- /dev/null
+# 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)
--- /dev/null
+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.
+
--- /dev/null
+#!/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 "$@"
--- /dev/null
+Manifest-Version: 1.0
+Main-Class: com.android.apkcheck.ApkCheck
--- /dev/null
+# 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)
+
--- /dev/null
+/*
+ * 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("<init>", "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;
+ }
+ }
+}
+
--- /dev/null
+/*
+ * 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<String,PackageInfo> 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<String,PackageInfo>();
+ 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<PackageInfo> getPackageIterator() {
+ return mPackageList.values().iterator();
+ }
+}
+
--- /dev/null
+/*
+ * 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<PackageInfo> pkgIter = pubList.getPackageIterator();
+ while (pkgIter.hasNext()) {
+ PackageInfo pubPkgInfo = pkgIter.next();
+
+ Iterator<ClassInfo> 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<PackageInfo> 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<ClassInfo> 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<FieldInfo> 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<MethodInfo> 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<PackageInfo> iter = apiList.getPackageIterator();
+ while (iter.hasNext()) {
+ PackageInfo pkgInfo = iter.next();
+ dumpPackage(pkgInfo);
+ }
+ }
+
+ private static void dumpPackage(PackageInfo pkgInfo) {
+ Iterator<ClassInfo> 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<FieldInfo> fieldIter = classInfo.getFieldIterator();
+ while (fieldIter.hasNext()) {
+ FieldInfo fieldInfo = fieldIter.next();
+ dumpField(fieldInfo);
+ }
+ Iterator<MethodInfo> 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());
+ }
+}
+
--- /dev/null
+/*
+ * 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 =
+ "<api>\n" +
+ " <package name=\"java.util\">\n" +
+ " <class name=\"EnumSet\"\n" +
+ " extends=\"java.util.AbstractSet\">\n" +
+ " <method name=\"of\" return=\"java.util.EnumSet\">\n" +
+ " <parameter name=\"e\" type=\"java.lang.Enum\"/>\n" +
+ " </method>\n" +
+ " <method name=\"of\" return=\"java.util.EnumSet\">\n" +
+ " <parameter name=\"e1\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e2\" type=\"java.lang.Enum\"/>\n" +
+ " </method>\n" +
+ " <method name=\"of\" return=\"java.util.EnumSet\">\n" +
+ " <parameter name=\"e1\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e2\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e3\" type=\"java.lang.Enum\"/>\n" +
+ " </method>\n" +
+ " <method name=\"of\" return=\"java.util.EnumSet\">\n" +
+ " <parameter name=\"e1\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e2\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e3\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e4\" type=\"java.lang.Enum\"/>\n" +
+ " </method>\n" +
+ " <method name=\"of\" return=\"java.util.EnumSet\">\n" +
+ " <parameter name=\"e1\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e2\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e3\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e4\" type=\"java.lang.Enum\"/>\n" +
+ " <parameter name=\"e5\" type=\"java.lang.Enum\"/>\n" +
+ " </method>\n" +
+ " </class>\n" +
+
+ " </package>\n" +
+ " <package name=\"android.os\">\n" +
+
+ " <class name=\"RemoteCallbackList\"\n" +
+ " extends=\"java.lang.Object\">\n" +
+ " <method name=\"register\" return=\"boolean\">\n" +
+ " <parameter name=\"callback\" type=\"android.os.IInterface\"/>\n" +
+ " </method>\n" +
+ " <method name=\"unregister\" return=\"boolean\">\n" +
+ " <parameter name=\"callback\" type=\"android.os.IInterface\"/>\n" +
+ " </method>\n" +
+ " </class>\n" +
+
+ " <class name=\"AsyncTask\"\n" +
+ " extends=\"java.lang.Object\">\n" +
+ " <method name=\"onPostExecute\" return=\"void\">\n" +
+ " <parameter name=\"result\" type=\"java.lang.Object\"/>\n" +
+ " </method>\n" +
+ " <method name=\"onProgressUpdate\" return=\"void\">\n" +
+ " <parameter name=\"values\" type=\"java.lang.Object[]\"/>\n" +
+ " </method>\n" +
+ " <method name=\"execute\" return=\"android.os.AsyncTask\">\n" +
+ " <parameter name=\"params\" type=\"java.lang.Object[]\"/>\n" +
+ " </method>\n" +
+ " </class>\n" +
+
+ " </package>\n" +
+ " <package name=\"android.widget\">\n" +
+
+ " <class name=\"AutoCompleteTextView\"\n" +
+ " extends=\"android.widget.EditText\">\n" +
+ " <method name=\"setAdapter\" return=\"void\">\n" +
+ " <parameter name=\"adapter\" type=\"android.widget.ListAdapter\"/>\n" +
+ " </method>\n" +
+ " </class>\n" +
+
+ " </package>\n" +
+ "</api>\n"
+ ;
+
+ /**
+ * Returns the built-in definition "file".
+ */
+ public static StringReader getReader() {
+ return new StringReader(BUILTIN);
+ }
+}
+
--- /dev/null
+/*
+ * 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<String,MethodInfo> mMethodList;
+ // fields are hashed on name:type
+ private HashMap<String,FieldInfo> 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<String> 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<String,MethodInfo>();
+ mFieldList = new HashMap<String,FieldInfo>();
+ mSuperNames = new ArrayList<String>();
+ 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<FieldInfo> 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<MethodInfo> 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<MethodInfo> 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<String> keyIter;
+
+ HashMap<String,FieldInfo> tmpFieldList = new HashMap<String,FieldInfo>();
+ 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<String,MethodInfo> tmpMethodList = new HashMap<String,MethodInfo>();
+ 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);
+ }
+}
+
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
+
--- /dev/null
+/*
+ * 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<String> 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<String>();
+ }
+
+ /**
+ * 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;
+ }
+ }
+}
+
--- /dev/null
+/*
+ * 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<String,ClassInfo> mClassList;
+
+ public PackageInfo(String name) {
+ mName = name;
+ mClassList = new HashMap<String,ClassInfo>();
+ }
+
+ 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<ClassInfo> getClassIterator() {
+ return mClassList.values().iterator();
+ }
+}
+
--- /dev/null
+/*
+ * 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<String,String> sQuickConvert;
+ static {
+ sQuickConvert = new HashMap<String,String>();
+
+ 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:
+ * <parameter name="collection" type="java.util.Collection<? extends E>">
+ * <parameter name="object" type="E">
+ *
+ * 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;
+ }
+}
+