OSDN Git Service

APK checker.
authorAndy McFadden <fadden@android.com>
Tue, 9 Feb 2010 00:48:01 +0000 (16:48 -0800)
committerAndy McFadden <fadden@android.com>
Fri, 19 Feb 2010 18:18:04 +0000 (10:18 -0800)
Compares the fields and methods accessed by an APK against the public
API files generated by the build.  See the README for details.

14 files changed:
tools/apkcheck/Android.mk [new file with mode: 0644]
tools/apkcheck/README.txt [new file with mode: 0644]
tools/apkcheck/etc/apkcheck [new file with mode: 0644]
tools/apkcheck/etc/manifest.txt [new file with mode: 0644]
tools/apkcheck/src/Android.mk [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/ApiDescrHandler.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/ApiList.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/ApkCheck.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/Builtin.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/ClassInfo.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/FieldInfo.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/MethodInfo.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/PackageInfo.java [new file with mode: 0644]
tools/apkcheck/src/com/android/apkcheck/TypeUtils.java [new file with mode: 0644]

diff --git a/tools/apkcheck/Android.mk b/tools/apkcheck/Android.mk
new file mode 100644 (file)
index 0000000..c388939
--- /dev/null
@@ -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 (file)
index 0000000..38e30d2
--- /dev/null
@@ -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 (file)
index 0000000..78af93a
--- /dev/null
@@ -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 (file)
index 0000000..aa4fef2
--- /dev/null
@@ -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 (file)
index 0000000..abc813a
--- /dev/null
@@ -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 (file)
index 0000000..427a20f
--- /dev/null
@@ -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 &lt;package&gt;.  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;
+        }
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/ApiList.java b/tools/apkcheck/src/com/android/apkcheck/ApiList.java
new file mode 100644 (file)
index 0000000..9eb7001
--- /dev/null
@@ -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<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();
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/ApkCheck.java b/tools/apkcheck/src/com/android/apkcheck/ApkCheck.java
new file mode 100644 (file)
index 0000000..42196a5
--- /dev/null
@@ -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<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());
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/Builtin.java b/tools/apkcheck/src/com/android/apkcheck/Builtin.java
new file mode 100644 (file)
index 0000000..8e3b196
--- /dev/null
@@ -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 =
+        "<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);
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/ClassInfo.java b/tools/apkcheck/src/com/android/apkcheck/ClassInfo.java
new file mode 100644 (file)
index 0000000..6ceb531
--- /dev/null
@@ -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<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);
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/FieldInfo.java b/tools/apkcheck/src/com/android/apkcheck/FieldInfo.java
new file mode 100644 (file)
index 0000000..4ab0665
--- /dev/null
@@ -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 (file)
index 0000000..79e4cd5
--- /dev/null
@@ -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<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;
+        }
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/PackageInfo.java b/tools/apkcheck/src/com/android/apkcheck/PackageInfo.java
new file mode 100644 (file)
index 0000000..533f917
--- /dev/null
@@ -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<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();
+    }
+}
+
diff --git a/tools/apkcheck/src/com/android/apkcheck/TypeUtils.java b/tools/apkcheck/src/com/android/apkcheck/TypeUtils.java
new file mode 100644 (file)
index 0000000..75e2bcd
--- /dev/null
@@ -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<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 --&gt; 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&lt;? extends E&gt;">
+         *   <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;
+    }
+}
+