Merge "APK checker."

This commit is contained in:
Andy McFadden
2010-02-19 12:01:25 -08:00
committed by Android (Google) Code Review
14 changed files with 1722 additions and 0 deletions

42
tools/apkcheck/Android.mk Normal file
View File

@@ -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)

39
tools/apkcheck/README.txt Normal file
View File

@@ -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.

View File

@@ -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 "$@"

View File

@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Main-Class: com.android.apkcheck.ApkCheck

View File

@@ -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)

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apkcheck;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.io.*;
/**
* Provides implementation for SAX parser.
*/
class ApiDescrHandler extends DefaultHandler {
/*
* Uber-container.
*/
private ApiList mApiList;
/*
* Temporary objects, used as containers while we accumulate the
* innards.
*/
private PackageInfo mCurrentPackage = null;
private ClassInfo mCurrentClass = null;
private MethodInfo mCurrentMethod = null;
/**
* Constructs an ApiDescrHandler.
*
* @param fileName Source file name, used for debugging.
*/
public ApiDescrHandler(ApiList apiList) {
mApiList = apiList;
}
/**
* Returns the ApiList in its current state. Generally only
* makes sense to call here after parsing is completed.
*/
public ApiList getApiList() {
return mApiList;
}
/**
* Processes start tags. If the file is malformed we will likely
* NPE, but this is captured by the caller.
*
* We currently assume that packages and classes only appear once,
* so all classes associated with a package are wrapped in a singular
* instance of <package>. We may want to remove this assumption
* by attempting to find an existing package/class with the same name.
*/
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) {
if (qName.equals("package")) {
/* top-most element */
mCurrentPackage = mApiList.getOrCreatePackage(
attributes.getValue("name"));
} else if (qName.equals("class") || qName.equals("interface")) {
/* get class, gather fields/methods and interfaces */
mCurrentClass = mCurrentPackage.getOrCreateClass(
attributes.getValue("name"),
attributes.getValue("extends"),
attributes.getValue("static"));
} else if (qName.equals("implements")) {
/* add name of interface to current class */
mCurrentClass.addInterface(attributes.getValue("name"));
} else if (qName.equals("method")) {
/* hold object while we gather parameters */
mCurrentMethod = new MethodInfo(attributes.getValue("name"),
attributes.getValue("return"));
} else if (qName.equals("constructor")) {
/* like "method", but has no name or return type */
mCurrentMethod = new MethodInfo("<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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}