AI 146498: am: CL 145983 am: CL 145911 ADT #1778786: tool to generate stubbed jar file.
  This is only a preliminary CL. More will follow but this is
  a good start, with the following caveats:
  What it does:
  - take an input jar, a list of includes, a list of excludes.
  - generate actual Java source for the filtered classes.
  What it doesn't do yet:
  - some more work on filtering inner elements (methods, etc.)
  - properly generate inner classes.
  - hide synthetic fields.
  - some classes body are missing
  - directly generate a stubbed bytecode/jar rather than source.
  I'll likely want to keep the source generator for debugging
  purposes or if we want to integrate with a build system instead.
  - classpath will be changed in the final CL to refer to the external
  ASM lib rather than the project. I need the source for debugging
  rigth now.
  - will review comments before submitting.
  Original author: raphael
  Merged from: //branches/cupcake/...
  Original author: android-build

Automated import of CL 146498
diff --git a/tools/mkstubs/.classpath b/tools/mkstubs/.classpath
new file mode 100644
index 0000000..a6b4c47
--- /dev/null
+++ b/tools/mkstubs/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="src" path="tests"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/asm3"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/tools/mkstubs/.project b/tools/mkstubs/.project
new file mode 100644
index 0000000..bef013e
--- /dev/null
+++ b/tools/mkstubs/.project
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>MkStubs</name>
+	<comment></comment>
+	<projects>
+		<project>asm3</project>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/tools/mkstubs/Android.mk b/tools/mkstubs/Android.mk
new file mode 100644
index 0000000..a12bf18
--- /dev/null
+++ b/tools/mkstubs/Android.mk
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2009 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)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_JAR_MANIFEST := manifest.txt
+LOCAL_STATIC_JAVA_LIBRARIES := \
+	asm-3.1
+
+LOCAL_MODULE := mkstubs
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/tools/mkstubs/manifest.txt b/tools/mkstubs/manifest.txt
new file mode 100644
index 0000000..c0ca8e6
--- /dev/null
+++ b/tools/mkstubs/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.mkstubs.Main
diff --git a/tools/mkstubs/src/com/android/mkstubs/AsmAnalyzer.java b/tools/mkstubs/src/com/android/mkstubs/AsmAnalyzer.java
new file mode 100644
index 0000000..c023cf2
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/AsmAnalyzer.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+import org.objectweb.asm.ClassReader;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * 
+ */
+class AsmAnalyzer {
+
+    /**
+     * Parses a JAR file and returns a list of all classes founds using a map
+     * class name => ASM ClassReader. Class names are in the form "android.view.View".
+     */
+    Map<String,ClassReader> parseInputJar(String inputJarPath) throws IOException {
+        TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>();
+
+        ZipFile zip = new ZipFile(inputJarPath);
+        Enumeration<? extends ZipEntry> entries = zip.entries();
+        ZipEntry entry;
+        while (entries.hasMoreElements()) {
+            entry = entries.nextElement();
+            if (entry.getName().endsWith(".class")) {
+                ClassReader cr = new ClassReader(zip.getInputStream(entry));
+                String className = classReaderToAsmName(cr);
+                classes.put(className, cr);
+            }
+        }
+        
+        return classes;
+    }
+
+    /**
+     * Utility that returns the fully qualified ASM class name for a ClassReader.
+     * E.g. it returns something like android/view/View.
+     */
+    static String classReaderToAsmName(ClassReader classReader) {
+        if (classReader == null) {
+            return null;
+        } else {
+            return classReader.getClassName();
+        }
+    }
+
+    public void filter(
+            Map<String, ClassReader> classes,
+            ArrayList<String> inclusions,
+            ArrayList<String> exclusions) {
+
+        ArrayList<String> inPrefix = new ArrayList<String>();
+        HashSet  <String> inFull   = new HashSet  <String>();
+        ArrayList<String> exPrefix = new ArrayList<String>();
+        HashSet  <String> exFull   = new HashSet  <String>();
+        
+        for (String in : inclusions) {
+            if (in.endsWith("*")) {
+                inPrefix.add(in.substring(0, in.length() - 1));
+            } else {
+                inFull.add(in);
+            }
+        }
+        
+        for (String ex : exclusions) {
+            if (ex.endsWith("*")) {
+                exPrefix.add(ex.substring(0, ex.length() - 1));
+            } else {
+                exFull.add(ex);
+            }
+        }
+        
+        
+        Set<String> keys = classes.keySet();
+        for(Iterator<String> it = keys.iterator(); it.hasNext(); ) {
+            String key = it.next();
+
+            
+            // Check if it can be included.
+            boolean keep = inFull.contains(key);
+            if (!keep) {
+                // Check for a prefix inclusion
+                for (String prefix : inPrefix) {
+                    if (key.startsWith(prefix)) {
+                        keep = true;
+                        break;
+                    }
+                }
+            }
+            
+            if (keep) {
+                // check for a full exclusion
+                keep = !exFull.contains(key);
+            }
+            if (keep) {
+                // or check for prefix exclusion
+                for (String prefix : exPrefix) {
+                    if (key.startsWith(prefix)) {
+                        keep = false;
+                        break;
+                    }
+                }
+            }
+            
+            // remove if we don't keep it
+            if (!keep) {
+                System.out.println("- Remove class " + key);
+                it.remove();
+            }
+        }
+    }
+    
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/AsmGenerator.java b/tools/mkstubs/src/com/android/mkstubs/AsmGenerator.java
new file mode 100644
index 0000000..3446b00
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/AsmGenerator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+import com.android.mkstubs.sourcer.JavaSourcer;
+import com.android.mkstubs.sourcer.Output;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * 
+ */
+class AsmGenerator {
+
+    /**
+     * Generate source for the stubbed classes, mostly for debug purposes.
+     * @throws IOException 
+     */
+    public void generateSource(File baseDir,
+            Map<String, ClassReader> classes,
+            List<String> exclusions) throws IOException {
+        
+        for (Entry<String, ClassReader> entry : classes.entrySet()) {
+            ClassReader cr = entry.getValue();
+            
+            String name = classNameToJavaPath(cr.getClassName());
+
+            FileWriter fw = null;
+            try {
+                fw = createWriter(baseDir, name);
+                dumpClass(fw, cr, exclusions);
+            } finally {
+                fw.close();
+            }
+        }
+    }
+
+    FileWriter createWriter(File baseDir, String name) throws IOException {
+        File f = new File(baseDir, name);
+        f.getParentFile().mkdirs();
+        
+        System.out.println("Writing " + f.getPath());
+        
+        return new FileWriter(f);
+    }
+
+    /**
+     * Utility method that converts a fully qualified java name into a JAR entry path
+     * e.g. for the input "android.view.View" it returns "android/view/View.java"
+     */
+    String classNameToJavaPath(String className) {
+        return className.replace('.', '/').concat(".java");
+    }
+
+    /**
+     * Generate a source equivalent to the stubbed version of the class reader,
+     * minus all exclusions
+     */
+    void dumpClass(Writer fw, ClassReader cr, List<String> exclusions) {
+        System.out.println("Dump " + cr.getClassName());
+        
+        ClassVisitor javaWriter = new JavaSourcer(new Output(fw));
+        ClassVisitor filter = new FilterClassAdapter(javaWriter, exclusions);
+        cr.accept(filter, 0 /*flags*/);
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/FilterClassAdapter.java b/tools/mkstubs/src/com/android/mkstubs/FilterClassAdapter.java
new file mode 100644
index 0000000..71c55f8
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/FilterClassAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassAdapter;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.util.List;
+
+/**
+ * A class visitor that filters out all the referenced exclusions
+ */
+class FilterClassAdapter extends ClassAdapter {
+
+    private final List<String> mExclusions;
+
+    public FilterClassAdapter(ClassVisitor writer, List<String> exclusions) {
+        super(writer);
+        mExclusions = exclusions;
+    }
+
+    @Override
+    public void visit(int version, int access, String name, String signature,
+            String superName, String[] interfaces) {
+
+        // TODO filter super type
+        // TODO filter interfaces 
+        
+        super.visit(version, access, name, signature, superName, interfaces);
+    }
+
+    @Override
+    public void visitEnd() {
+        super.visitEnd();
+    }
+    
+    /**
+     * Visits a field.
+     * 
+     * {@inheritDoc}
+     * 
+     * Examples:
+     * name = mArg
+     * desc = Ljava/Lang/String;
+     * signature = null (not a template) or template type
+     */
+    @Override
+    public FieldVisitor visitField(int access, String name, String desc,
+            String signature, Object value) {
+        // exclude private fields
+        if ((access & Opcodes.ACC_PRIVATE) != 0) {
+            return null;
+        }
+        
+        // TODO filter on name
+
+        return super.visitField(access, name, desc, signature, value);
+    }
+
+    /**
+     * Visits a method.
+     * 
+     * {@inheritDoc}
+     * 
+     * Examples:
+     * name = <init>
+     * desc = ()V
+     * signature = null (not a template) or template type
+     */
+    @Override
+    public MethodVisitor visitMethod(int access, String name, String desc,
+            String signature, String[] exceptions) {
+
+        // exclude private methods
+        if ((access & Opcodes.ACC_PRIVATE) != 0) {
+            return null;
+        }
+        
+        // TODO filter exceptions: error if filtered exception is being used
+
+        // TODO filter on name; error if filtered desc or signatures is being used
+
+        return super.visitMethod(access, name, desc, signature, exceptions);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+        
+        // Filter on desc type
+        return super.visitAnnotation(desc, visible);
+    }
+
+    @Override
+    public void visitAttribute(Attribute attr) {
+        // pass
+    }
+
+    @Override
+    public void visitInnerClass(String name, String outerName, String innerName, int access) {
+
+        // exclude private methods
+        if ((access & Opcodes.ACC_PRIVATE) != 0) {
+            return;
+        }
+
+        // TODO filter on name
+
+        super.visitInnerClass(name, outerName, innerName, access);
+    }
+
+    @Override
+    public void visitOuterClass(String owner, String name, String desc) {
+        // TODO Auto-generated method stub
+    }
+
+    @Override
+    public void visitSource(String source, String debug) {
+        // pass
+    }
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/Main.java b/tools/mkstubs/src/com/android/mkstubs/Main.java
new file mode 100644
index 0000000..017b2f1
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/Main.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+import org.objectweb.asm.ClassReader;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+
+/**
+ * 
+ */
+public class Main {
+    
+    static class Params {
+        private String mInputJarPath;
+        private String mOutputJarPath;
+        private ArrayList<String> mInclusions = new ArrayList<String>();
+        private ArrayList<String> mExclusions = new ArrayList<String>();
+
+        public Params(String inputJarPath, String outputJarPath) {
+            mInputJarPath = inputJarPath;
+            mOutputJarPath = outputJarPath;
+        }
+        
+        public String getInputJarPath() {
+            return mInputJarPath;
+        }
+
+        public String getOutputJarPath() {
+            return mOutputJarPath;
+        }
+
+        public ArrayList<String> getExclusions() {
+            return mExclusions;
+        }
+        
+        public ArrayList<String> getInclusions() {
+            return mInclusions;
+        }
+    }
+    
+    /**
+     * @param args
+     */
+    public static void main(String[] args) {
+
+        Main m = new Main();
+        try {
+            Params p = m.processArgs(args);
+            m.process(p);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private Params processArgs(String[] args) throws IOException {
+        
+        if (args.length < 2) {
+            usage();
+        }
+
+        Params p = new Params(args[0], args[1]);
+        
+        for (int i = 2; i < args.length; i++) {
+            String s = args[i];
+            if (s.startsWith("@")) {
+                addStringsFromFile(p, s.substring(1));
+            } else if (s.startsWith("-")) {
+                p.getExclusions().add(s.substring(1));
+            } else if (s.startsWith("+")) {
+                p.getInclusions().add(s.substring(1));
+            }
+        }
+        
+        return p;
+    }
+
+    private void addStringsFromFile(Params p, String inputFile)
+            throws IOException {
+        BufferedReader br = null;
+        try {
+            br = new BufferedReader(new FileReader(inputFile));
+            String line;
+            while ((line = br.readLine()) != null) {
+                line = line.trim();
+                if (line.length() == 0) {
+                    continue;
+                }
+                char mode = line.charAt(0);
+                line = line.substring(1).trim();
+                
+                if (line.length() > 0) {
+                    // Keep all class names in ASM path-like format, e.g. android/view/View
+                    line = line.replace('.', '/');
+                    if (mode == '-') {
+                        p.getExclusions().add(line);
+                    } else if (mode == '+') {
+                        p.getInclusions().add(line);
+                    }
+                }
+            }
+        } finally {
+            br.close();
+        }
+    }
+
+    private void usage() {
+        System.out.println("Usage: mkstub input.jar output.jar [excluded-class @excluded-classes-file ...]");
+
+        System.out.println("Include syntax:\n" +
+                "+com.package.* : whole package, with glob\n" +
+                "+com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" +
+                "Inclusion is not supported at method/field level.\n\n");
+
+        System.out.println("Exclude syntax:\n" +
+        		"-com.package.* : whole package, with glob\n" +
+        		"-com.package.Class[$Inner] or ...Class*: whole classes with optional glob\n" +
+        		"-com.package.Class#method: whole method or field\n" +
+                "-com.package.Class#method(IILjava/lang/String;)V: specific method with signature.\n\n");
+        System.exit(1);
+    }
+
+    private void process(Params p) throws IOException {
+        AsmAnalyzer aa = new AsmAnalyzer();
+        Map<String, ClassReader> classes = aa.parseInputJar(p.getInputJarPath());
+     
+        aa.filter(classes, p.getInclusions(), p.getExclusions());
+
+        AsmGenerator gen = new AsmGenerator();
+
+        // dump as Java source files, mostly for debugging
+        File dst_src_dir = new File(p.getOutputJarPath() + File.separator + "sources");
+        dst_src_dir.mkdir();
+        gen.generateSource(dst_src_dir, classes, p.getExclusions());
+        
+    }
+
+    /** @deprecated debug only */
+    private void displayClasses(Map<String, ClassReader> classes) {
+        for(String className : classes.keySet()) {
+            System.out.println("Found " + className);
+        }
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/AccessSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/AccessSourcer.java
new file mode 100644
index 0000000..757dcea
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/AccessSourcer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.Opcodes;
+
+/**
+ * 
+ */
+class AccessSourcer {
+
+    private final Output mOutput;
+
+    public static int IS_CLASS  = 1;
+    public static int IS_FIELD  = 2;
+    public static int IS_METHOD = 4;
+    
+    private enum Flag {
+        ACC_PUBLIC(Opcodes.ACC_PUBLIC               , IS_CLASS | IS_FIELD | IS_METHOD),
+        ACC_PRIVATE(Opcodes.ACC_PRIVATE             , IS_CLASS | IS_FIELD | IS_METHOD),
+        ACC_PROTECTED(Opcodes.ACC_PROTECTED         , IS_CLASS | IS_FIELD | IS_METHOD),
+        ACC_STATIC(Opcodes.ACC_STATIC               , IS_FIELD | IS_METHOD),
+        ACC_FINAL(Opcodes.ACC_FINAL                 , IS_CLASS | IS_FIELD | IS_METHOD),
+        ACC_SUPER(Opcodes.ACC_SUPER                 , IS_CLASS),
+        ACC_SYNCHRONIZED(Opcodes.ACC_SYNCHRONIZED   , IS_METHOD),
+        ACC_VOLATILE(Opcodes.ACC_VOLATILE           , IS_FIELD),
+        ACC_BRIDGE(Opcodes.ACC_BRIDGE               , IS_METHOD),
+        ACC_VARARGS(Opcodes.ACC_VARARGS             , IS_METHOD),
+        ACC_TRANSIENT(Opcodes.ACC_TRANSIENT         , IS_FIELD),
+        ACC_NATIVE(Opcodes.ACC_NATIVE               , IS_METHOD),
+        ACC_INTERFACE(Opcodes.ACC_INTERFACE         , IS_CLASS),
+        ACC_ABSTRACT(Opcodes.ACC_ABSTRACT           , IS_CLASS | IS_METHOD),
+        ACC_STRICT(Opcodes.ACC_STRICT               , IS_METHOD),
+        ACC_SYNTHETIC(Opcodes.ACC_SYNTHETIC         , IS_CLASS | IS_FIELD | IS_METHOD),
+        ACC_ANNOTATION(Opcodes.ACC_ANNOTATION       , IS_CLASS),
+        ACC_ENUM(Opcodes.ACC_ENUM                   , IS_CLASS),
+        ACC_DEPRECATED(Opcodes.ACC_DEPRECATED       , IS_CLASS | IS_FIELD | IS_METHOD)
+        ;
+        
+        private final int mValue;
+        private final int mFilter;
+
+        private Flag(int value, int filter) {
+            mValue = value;
+            mFilter = filter;
+        }
+        
+        public int getValue() {
+            return mValue;
+        }
+        
+        public int getFilter() {
+            return mFilter;
+        }
+        
+        /** Transforms "ACC_PUBLIC" into "public" */
+        @Override
+        public String toString() {
+            return super.toString().substring(4).toLowerCase();
+        }
+    }
+
+    
+    public AccessSourcer(Output output) {
+        mOutput = output;
+    }
+
+    /**
+     * Generates a list of access keywords, e.g. "public final".
+     *  
+     * @param access The access mode, e.g. 33 or 18
+     * @param filter One of {@link #IS_CLASS}, {@link #IS_FIELD} or {@link #IS_METHOD}, which
+     *        indicates the validity context.
+     */
+    public void write(int access, int filter) {
+
+        boolean need_sep = false;
+        
+        for (Flag f : Flag.values()) {
+            if ((f.getFilter() & filter) != 0 && (access & f.getValue()) != 0) {
+                if (need_sep) {
+                    mOutput.write(" ");
+                }
+                mOutput.write(f.toString());
+                need_sep = true;
+            }
+        }
+ 
+    }
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/AnnotationSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/AnnotationSourcer.java
new file mode 100644
index 0000000..bbf1cb2
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/AnnotationSourcer.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.AnnotationVisitor;
+
+/**
+ * 
+ */
+class AnnotationSourcer implements AnnotationVisitor {
+
+    private final String mOpenChar;
+    private final String mCloseChar;
+    private final Output mOutput;
+    private boolean mNeedClose;
+
+    public AnnotationSourcer(Output output) {
+        this(output, false /*isArray*/);
+    }
+
+    public AnnotationSourcer(Output output, boolean isArray) {
+        mOutput = output;
+        mOpenChar = isArray ? "[" : "(";
+        mCloseChar = isArray ? "]" : ")";
+    }
+
+    public void visit(String name, Object value) {
+        startOpen();
+
+        if (name != null) {
+            mOutput.write("%s=", name);
+        }
+        if (value != null) {
+            mOutput.write(name.toString());
+        }
+    }
+
+    private void startOpen() {
+        if (!mNeedClose) {
+            mNeedClose = true;
+            mOutput.write(mOpenChar);
+        }
+    }
+
+    public void visitEnd() {
+        if (mNeedClose) {
+            mOutput.write(mCloseChar);
+        }
+        mOutput.write("\n");
+    }
+
+    public AnnotationVisitor visitAnnotation(String name, String desc) {
+        startOpen();
+        
+        mOutput.write("@%s", name);
+        return this;
+    }
+
+    public AnnotationVisitor visitArray(String name) {
+        startOpen();
+        return new AnnotationSourcer(mOutput, true /*isArray*/);
+    }
+
+    public void visitEnum(String name, String desc, String value) {
+        mOutput.write("/* annotation enum not supported: %s */\n", name);
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/FieldSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/FieldSourcer.java
new file mode 100644
index 0000000..4f9c229
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/FieldSourcer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.signature.SignatureReader;
+
+/**
+ * 
+ */
+class FieldSourcer implements FieldVisitor {
+
+    private final Output mOutput;
+    private final int mAccess;
+    private final String mName;
+    private final String mDesc;
+    private final String mSignature;
+
+    public FieldSourcer(Output output, int access, String name, String desc, String signature) {
+        mOutput = output;
+        mAccess = access;
+        mName = name;
+        mDesc = desc;
+        mSignature = signature;
+    }
+
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+        mOutput.write("@%s", desc);
+        return new AnnotationSourcer(mOutput);
+    }
+
+    public void visitAttribute(Attribute attr) {
+        mOutput.write("%s /* non-standard attribute */ ", attr.type);
+    }
+
+    public void visitEnd() {
+        // Need to write type and field name after the annotations and attributes.
+
+        AccessSourcer as = new AccessSourcer(mOutput);
+        as.write(mAccess, AccessSourcer.IS_FIELD);
+        
+        if (mSignature == null) {
+            mOutput.write(" %s", mOutput.decodeDesc(mDesc));
+        } else {
+            mOutput.write(" ");
+            SignatureReader sigReader = new SignatureReader(mSignature);
+            SignatureSourcer sigSourcer = new SignatureSourcer();
+            sigReader.acceptType(sigSourcer);
+            mOutput.write(sigSourcer.toString());
+        }
+
+        mOutput.write(" %s", mName);
+
+        mOutput.write(";\n");
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/JavaSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/JavaSourcer.java
new file mode 100644
index 0000000..fb06c68
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/JavaSourcer.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.signature.SignatureReader;
+
+/**
+ * A class visitor that rewrites a java source
+ */
+public class JavaSourcer implements ClassVisitor {
+
+    private final Output mOutput;
+    private final AccessSourcer mAccessSourcer;
+    private String mClassName;
+
+    public JavaSourcer(Output output) {
+        mOutput = output;
+        mAccessSourcer = new AccessSourcer(mOutput);
+    }
+    
+    /* Examples:
+     * name = com/foo/MyClass
+     * signature = null (if not generic)
+     * superName = java/lang/Object
+     * interfaces = [ java/lang/Runnable ... ]
+     */
+    public void visit(int version, int access, String name, String signature,
+            String superName, String[] interfaces) {
+
+        String pkg = name.substring(0, name.lastIndexOf('/')).replace('/', '.');
+        mClassName = name.substring(name.lastIndexOf('/') + 1);
+
+        mOutput.write("package %s;\n", pkg);
+
+        // dump access keywords. Note: do not dump "super" here
+        mAccessSourcer.write(access & ~Opcodes.ACC_SUPER, AccessSourcer.IS_CLASS);
+
+        // write class name
+        mOutput.write(" class %s", mClassName);
+
+        if (signature != null) {
+            // write template formal definition and super type
+            SignatureReader sigReader = new SignatureReader(signature);
+            SignatureSourcer sigSourcer = new SignatureSourcer();
+            sigReader.accept(sigSourcer);
+            
+            if (sigSourcer.hasFormalsContent()) {
+                mOutput.write(sigSourcer.formalsToString());
+            }
+
+            mOutput.write(" extends %s", sigSourcer.getSuperClass().toString());
+            
+        } else {
+            // write non-generic super type
+            mOutput.write(" extends %s", superName.replace('/', '.'));
+        }
+
+        // write interfaces defined, if any
+        if (interfaces != null && interfaces.length > 0) {
+            mOutput.write(" implements ");
+            boolean need_sep = false;
+            for (String i : interfaces) {
+                if (need_sep) {
+                    mOutput.write(", ");
+                }
+                mOutput.write(i.replace('/', '.'));
+                need_sep = true;
+            }
+        }
+        
+        // open class body
+        mOutput.write(" {\n");
+    }
+
+    public void visitEnd() {
+        mOutput.write("}\n");
+    }
+
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+        mOutput.write("@%s", desc);
+        return new AnnotationSourcer(mOutput);
+    }
+
+    public void visitAttribute(Attribute attr) {
+        mOutput.write("%s /* non-standard class attribute */ ", attr.type);
+    }
+
+
+    public FieldVisitor visitField(int access, String name, String desc, String signature,
+            Object value) {
+        // skip synthetic fields
+        if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
+            return null;
+        }
+        
+        return new FieldSourcer(mOutput, access, name, desc, signature);
+    }
+
+    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
+            String[] exceptions) {
+
+        // Visit the method and dump its stub.
+        return new MethodSourcer(mOutput, mClassName, access, name, desc, signature, exceptions);
+    }
+
+    public void visitInnerClass(String name, String outerName, String innerName, int access) {
+        // Skip inner classes. This just indicates there's an inner class definition but
+        // they are visited at the top level as separate classes.
+    }
+
+    public void visitOuterClass(String owner, String name, String desc) {
+        // Skip outer classes.
+    }
+
+    public void visitSource(String source, String debug) {
+        // Skip source information.
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/MethodSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/MethodSourcer.java
new file mode 100644
index 0000000..d474e4b
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/MethodSourcer.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.signature.SignatureReader;
+
+import java.util.ArrayList;
+
+/**
+ * 
+ */
+class MethodSourcer implements MethodVisitor {
+
+    private final Output mOutput;
+    private final int mAccess;
+    private final String mClassName;
+    private final String mName;
+    private final String mDesc;
+    private final String mSignature;
+    private final String[] mExceptions;
+    private boolean mNeedDeclaration;
+    private boolean mIsConstructor;
+
+    public MethodSourcer(Output output, String className, int access, String name,
+            String desc, String signature, String[] exceptions) {
+        mOutput = output;
+        mClassName = className;
+        mAccess = access;
+        mName = name;
+        mDesc = desc;
+        mSignature = signature;
+        mExceptions = exceptions;
+        
+        mNeedDeclaration = true;
+        mIsConstructor = "<init>".equals(name);
+    }
+    
+    private void writeHeader() {        
+        if (!mNeedDeclaration) {
+            return;
+        }
+        
+        AccessSourcer as = new AccessSourcer(mOutput);
+        as.write(mAccess, AccessSourcer.IS_METHOD);
+
+        // preprocess the signature to get the return type and the arguments
+        SignatureSourcer sigSourcer = null;
+        if (mSignature != null) {
+            SignatureReader sigReader = new SignatureReader(mSignature);
+            sigSourcer = new SignatureSourcer();
+            sigReader.accept(sigSourcer);
+            
+            if (sigSourcer.hasFormalsContent()) {
+                // dump formal template parameter definitions
+                mOutput.write(" %s", sigSourcer.formalsToString());
+            }
+        }
+        
+        // output return type (constructor have no return type)
+        if (!mIsConstructor) {
+            // The signature overrides desc, if present
+            if (sigSourcer == null || sigSourcer.getReturnType() == null) {
+                mOutput.write(" %s", Type.getReturnType(mDesc).getClassName());
+                
+            } else {
+                mOutput.write(" %s", sigSourcer.getReturnType().toString());
+            }
+        }
+
+        // output name
+        mOutput.write(" %s(", mIsConstructor ? mClassName : mName);
+            
+        // output arguments. The signature overrides desc, if present
+        if (mSignature == null) {
+            Type[] types = Type.getArgumentTypes(mDesc);
+            
+            for(int i = 0; i < types.length; i++) {
+                if (i > 0) {
+                    mOutput.write(", ");
+                }
+                mOutput.write("%s arg%d", types[i].getClassName(), i);
+            }
+        } else {
+            ArrayList<SignatureSourcer> params = sigSourcer.getParameters();
+            
+            for(int i = 0; i < params.size(); i++) {
+                if (i > 0) {
+                    mOutput.write(", ");
+                }
+                mOutput.write("%s arg%d", params.get(i).toString(), i);
+            }
+        }
+        mOutput.write(")");
+
+        // output throwable exceptions
+        if (mExceptions != null && mExceptions.length > 0) {
+            mOutput.write(" throws ");
+            
+            for (int i = 0; i < mExceptions.length; i++) {
+                if (i > 0) {
+                    mOutput.write(", ");
+                }
+                mOutput.write(mExceptions[i].replace('/', '.'));
+            }
+        }
+
+        mOutput.write(" {\n");
+
+        mNeedDeclaration = false;
+    }
+
+    public void visitCode() {
+        writeHeader();
+        
+        // write the stub itself
+        mOutput.write("throw new RuntimeException(\"Stub\");");
+    }
+
+    public void visitEnd() {
+        writeHeader();
+        mOutput.write("\n}\n");
+    }
+
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+        mOutput.write("@%s", desc);
+        return new AnnotationSourcer(mOutput);
+    }
+
+    public AnnotationVisitor visitAnnotationDefault() {
+        // pass
+        return null;
+    }
+
+    public void visitAttribute(Attribute attr) {
+        mOutput.write("%s /* non-standard method attribute */ ", attr.type);
+    }
+
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+        // pass
+    }
+
+    public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) {
+        // pass
+    }
+
+    public void visitIincInsn(int var, int increment) {
+        // pass
+    }
+
+    public void visitInsn(int opcode) {
+        // pass
+    }
+
+    public void visitIntInsn(int opcode, int operand) {
+        // pass
+    }
+
+    public void visitJumpInsn(int opcode, Label label) {
+        // pass
+    }
+
+    public void visitLabel(Label label) {
+        // pass
+    }
+
+    public void visitLdcInsn(Object cst) {
+        // pass
+    }
+
+    public void visitLineNumber(int line, Label start) {
+        // pass
+    }
+
+    public void visitLocalVariable(String name, String desc, String signature,
+            Label start, Label end, int index) {
+        // pass
+    }
+
+    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+        // pass
+    }
+
+    public void visitMaxs(int maxStack, int maxLocals) {
+        // pass
+    }
+
+    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+        // pass
+    }
+
+    public void visitMultiANewArrayInsn(String desc, int dims) {
+        // pass
+    }
+
+    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+        // pass
+        return null;
+    }
+
+    public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) {
+        // pass
+    }
+
+    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+        // pass
+    }
+
+    public void visitTypeInsn(int opcode, String type) {
+        // pass
+    }
+
+    public void visitVarInsn(int opcode, int var) {
+        // pass
+    }
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/Output.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/Output.java
new file mode 100644
index 0000000..837cf95
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/Output.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * 
+ */
+public class Output {
+    
+    private final Writer mWriter;
+
+    public Output(Writer writer) {
+        mWriter = writer;
+    }
+
+    public void write(String format, Object... args) {
+        try {
+            mWriter.write(String.format(format, args));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    public void write(char c) {
+        write(Character.toString(c));
+    }
+    
+    public void write(StringBuilder sb) {
+        write(sb.toString());
+    }
+
+    
+    public String decodeDesc(String desc) {
+        return Type.getType(desc).getClassName();
+    }
+
+
+}
diff --git a/tools/mkstubs/src/com/android/mkstubs/sourcer/SignatureSourcer.java b/tools/mkstubs/src/com/android/mkstubs/sourcer/SignatureSourcer.java
new file mode 100644
index 0000000..6202528
--- /dev/null
+++ b/tools/mkstubs/src/com/android/mkstubs/sourcer/SignatureSourcer.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.objectweb.asm.Type;
+import org.objectweb.asm.signature.SignatureReader;
+import org.objectweb.asm.signature.SignatureVisitor;
+import org.objectweb.asm.signature.SignatureWriter;
+
+import java.util.ArrayList;
+
+/**
+ * Note: most of the implementation is a duplicate of
+ * ASM's SignatureWriter with some slight variations.
+ * <p/>
+ * Note: When processing a method's signature, the signature order is the
+ * reverse of the source order, e.g. it is (parameters)return-type where
+ * we want to generate "return-type method-name (parameters)".
+ * So in this case the return-type and parameters are not output directly
+ * but are instead accumulated in internal variables.
+ */
+class SignatureSourcer implements SignatureVisitor {
+
+    /**
+     * Buffer used to construct the signature.
+     */
+    private final StringBuilder mBuf = new StringBuilder();
+
+    /**
+     * Buffer used to construct the formals signature.
+     */
+    private final StringBuilder mFormalsBuf = new StringBuilder();
+
+    /**
+     * Indicates if the signature is currently processing formal type parameters.
+     */
+    private boolean mWritingFormals;
+
+    /**
+     * Stack used to keep track of class types that have arguments. Each element
+     * of this stack is a boolean encoded in one bit. The top of the stack is
+     * the lowest order bit. Pushing false = *2, pushing true = *2+1, popping =
+     * /2.
+     */
+    private int mArgumentStack;
+
+    private SignatureSourcer mReturnType;
+
+    private SignatureSourcer mSuperClass;
+
+    private ArrayList<SignatureSourcer> mParameters = new ArrayList<SignatureSourcer>();
+
+
+    
+    /**
+     * Constructs a new {@link SignatureWriter} object.
+     */
+    public SignatureSourcer() {
+    }
+    
+    private StringBuilder getBuf() {
+        if (mWritingFormals) {
+            return mFormalsBuf;
+        } else {
+            return mBuf;
+        }
+    }
+
+    /**
+     * Contains the whole signature type when called by
+     * {@link SignatureReader#acceptType(SignatureVisitor)} or just the formals if
+     * called by {@link SignatureReader#accept(SignatureVisitor)}.
+     */
+    @Override
+    public String toString() {
+        return mBuf.toString();
+    }
+
+    /**
+     * Will be non-null if a return type was processed
+     * by {@link SignatureReader#accept(SignatureVisitor)}
+     */
+    public SignatureSourcer getReturnType() {
+        return mReturnType;
+    }
+    
+    /**
+     * Will be non-empty if a parameters were processed
+     * by {@link SignatureReader#accept(SignatureVisitor)}
+     */
+    public ArrayList<SignatureSourcer> getParameters() {
+        return mParameters;
+    }
+    
+    /**
+     * True if the signature contains formal type parameters, which are available 
+     * via {@link #formalsToString()} after calling {@link SignatureReader#accept(SignatureVisitor)}
+     */
+    public boolean hasFormalsContent() {
+        return mFormalsBuf.length() > 0;
+    }
+    
+    public String formalsToString() {
+        return mFormalsBuf.toString();
+    }
+    
+    /**
+     * Will be non-null if a super class was processed
+     * by {@link SignatureReader#accept(SignatureVisitor)}
+     */
+    public SignatureSourcer getSuperClass() {
+        return mSuperClass;
+    }
+
+    // ------------------------------------------------------------------------
+    // Implementation of the SignatureVisitor interface
+    // ------------------------------------------------------------------------
+
+    public void visitFormalTypeParameter(final String name) {
+        if (!mWritingFormals) {
+            mWritingFormals = true;
+            getBuf().append('<');
+        } else {
+            getBuf().append(", ");
+        }
+        getBuf().append(name);
+        getBuf().append(" extends ");
+    }
+
+    public SignatureVisitor visitClassBound() {
+        // we don't differentiate between visiting a sub class or interface type 
+        return this;
+    }
+
+    public SignatureVisitor visitInterfaceBound() {
+        // we don't differentiate between visiting a sub class or interface type 
+        return this;
+    }
+
+    public SignatureVisitor visitSuperclass() {
+        endFormals();
+        SignatureSourcer sourcer = new SignatureSourcer();
+        assert mSuperClass == null;
+        mSuperClass = sourcer;
+        return sourcer;
+    }
+
+    public SignatureVisitor visitInterface() {
+        return this;
+    }
+
+    public SignatureVisitor visitParameterType() {
+        endFormals();
+        SignatureSourcer sourcer = new SignatureSourcer();
+        mParameters.add(sourcer);
+        return sourcer;
+    }
+
+    public SignatureVisitor visitReturnType() {
+        endFormals();
+        SignatureSourcer sourcer = new SignatureSourcer();
+        assert mReturnType == null;
+        mReturnType = sourcer;
+        return sourcer;
+    }
+
+    public SignatureVisitor visitExceptionType() {
+        getBuf().append('^');
+        return this;
+    }
+
+    public void visitBaseType(final char descriptor) {
+        getBuf().append(Type.getType(Character.toString(descriptor)).getClassName());
+    }
+
+    public void visitTypeVariable(final String name) {
+        getBuf().append(name.replace('/', '.'));
+    }
+
+    public SignatureVisitor visitArrayType() {
+        getBuf().append('[');
+        return this;
+    }
+
+    public void visitClassType(final String name) {
+        getBuf().append(name.replace('/', '.'));
+        mArgumentStack *= 2;
+    }
+
+    public void visitInnerClassType(final String name) {
+        endArguments();
+        getBuf().append('.');
+        getBuf().append(name.replace('/', '.'));
+        mArgumentStack *= 2;
+    }
+
+    public void visitTypeArgument() {
+        if (mArgumentStack % 2 == 0) {
+            ++mArgumentStack;
+            getBuf().append('<');
+        } else {
+            getBuf().append(", ");
+        }
+        getBuf().append('*');
+    }
+
+    public SignatureVisitor visitTypeArgument(final char wildcard) {
+        if (mArgumentStack % 2 == 0) {
+            ++mArgumentStack;
+            getBuf().append('<');
+        } else {
+            getBuf().append(", ");
+        }
+        if (wildcard != '=') {
+            if (wildcard == '+') {
+                getBuf().append("? extends ");
+            } else if (wildcard == '-') {
+                getBuf().append("? super ");
+            } else {
+                // can this happen?
+                getBuf().append(wildcard);
+            }
+        }
+        return this;
+    }
+
+    public void visitEnd() {
+        endArguments();
+    }
+
+    // ------------------------------------------------------------------------
+    // Utility methods
+    // ------------------------------------------------------------------------
+
+    /**
+     * Ends the formal type parameters section of the signature.
+     */
+    private void endFormals() {
+        if (mWritingFormals) {
+            getBuf().append('>');
+            mWritingFormals = false;
+        }
+    }
+
+    /**
+     * Ends the type arguments of a class or inner class type.
+     */
+    private void endArguments() {
+        if (mArgumentStack % 2 != 0) {
+            getBuf().append('>');
+        }
+        mArgumentStack /= 2;
+    }
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/AsmGeneratorTest.java b/tools/mkstubs/tests/com/android/mkstubs/AsmGeneratorTest.java
new file mode 100644
index 0000000..dd079d5
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/AsmGeneratorTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+
+import java.io.StringWriter;
+import java.util.ArrayList;
+
+/**
+ * 
+ */
+public class AsmGeneratorTest {
+
+    private AsmGenerator mGen;
+
+    @Before
+    public void setUp() throws Exception {
+        mGen = new AsmGenerator();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    
+    @Test
+    public void testDumpClass() throws Exception {
+        StringWriter sw = new StringWriter();
+        ClassReader cr = new ClassReader("data/TestBaseClass");
+        
+        mGen.dumpClass(sw, cr, new ArrayList<String>());
+        
+        String s = sw.toString();
+        Assert.assertNotNull(s);
+    }
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/FilterClassAdapterTest.java b/tools/mkstubs/tests/com/android/mkstubs/FilterClassAdapterTest.java
new file mode 100644
index 0000000..d27c1d5
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/FilterClassAdapterTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 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.mkstubs;
+
+
+import org.junit.After;
+import org.junit.Before;
+
+public class FilterClassAdapterTest {
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/AccessSourcerTest.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/AccessSourcerTest.java
new file mode 100644
index 0000000..58022a9
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/AccessSourcerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.Opcodes;
+
+import java.io.StringWriter;
+
+public class AccessSourcerTest {
+
+    private StringWriter mWriter;
+    private AccessSourcer mSourcer;
+
+
+    @Before
+    public void setUp() throws Exception {
+        mWriter = new StringWriter();
+        mSourcer = new AccessSourcer(new Output(mWriter));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mWriter = null;
+        mSourcer = null;
+    }
+    
+    @Test
+    public void testAbstractPublic() throws Exception {
+        mSourcer.write(Opcodes.ACC_ABSTRACT | Opcodes.ACC_PUBLIC, AccessSourcer.IS_CLASS);
+
+        String s = mWriter.toString();
+        Assert.assertEquals("public abstract", s);
+    }
+    
+    @Test
+    public void testPrivateFinalStatic() throws Exception {
+        mSourcer.write(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC,
+                AccessSourcer.IS_METHOD);
+
+        String s = mWriter.toString();
+        Assert.assertEquals("private static final", s);
+    }
+    
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/FieldSourcerTest.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/FieldSourcerTest.java
new file mode 100644
index 0000000..a45a47b
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/FieldSourcerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.Opcodes;
+
+import java.io.StringWriter;
+
+/**
+ * 
+ */
+public class FieldSourcerTest {
+
+    private StringWriter mWriter;
+
+    @Before
+    public void setUp() throws Exception {
+        mWriter = new StringWriter();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mWriter = null;
+    }
+    
+    @Test
+    public void testStringField() throws Exception {
+        
+        FieldSourcer fs = new FieldSourcer(new Output(mWriter),
+                Opcodes.ACC_PUBLIC, // access
+                "mArg", // name
+                "Ljava/lang/String;", // desc
+                null // signature
+                );
+        fs.visitEnd();
+
+        String s = mWriter.toString();
+        Assert.assertEquals("public java.lang.String mArg;\n", s);
+    }
+    
+    @Test
+    public void testTemplateTypeField() throws Exception {
+        
+        FieldSourcer fs = new FieldSourcer(new Output(mWriter),
+                Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, // access
+                "mList", // name
+                "Ljava/util/ArrayList;", // desc
+                "Ljava/util/ArrayList<Ljava/lang/String;>;" // signature
+                );
+        fs.visitEnd();
+
+        String s = mWriter.toString();
+        Assert.assertEquals("private final java.util.ArrayList<java.lang.String> mList;\n", s);
+    }
+
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/JavaSourcerTest.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/JavaSourcerTest.java
new file mode 100644
index 0000000..250da2a
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/JavaSourcerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+
+import java.io.StringWriter;
+
+/**
+ * 
+ */
+public class JavaSourcerTest extends TestHelper {
+
+    /**
+     * @throws java.lang.Exception
+     */
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    /**
+     * @throws java.lang.Exception
+     */
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testBaseClassSource() throws Exception {
+        StringWriter sw = new StringWriter();
+        ClassReader cr = new ClassReader("data/TestBaseClass");
+        
+        JavaSourcer jw = new JavaSourcer(new Output(sw));
+        cr.accept(jw, 0);
+        
+        assertSourceEquals(
+                "package data;\n" + 
+        		"public class TestBaseClass extends java.lang.Object implements java.lang.Runnable {\n" + 
+        		"\n" + 
+        		"    private final java.lang.String mArg;\n" + 
+        		"    \n" + 
+        		"    public TestBaseClass() {\n" + 
+        		"        throw new RuntimeException(\"Stub\");" +
+        		"    }\n" + 
+        		"    public TestBaseClass(java.lang.String arg0) {\n" +
+                "        throw new RuntimeException(\"Stub\");" +
+        		"    }\n" +
+        		"    public java.lang.String getArg() {\n" +
+                "        throw new RuntimeException(\"Stub\");" +
+        		"    }\n" +
+        		"    public void run() {\n" + 
+                "        throw new RuntimeException(\"Stub\");" +
+        		"    }\n" + 
+        		"}",
+        		sw.toString());
+    }
+
+    @Test
+    public void testInnerClassSource() throws Exception {
+        StringWriter sw = new StringWriter();
+        ClassReader cr = new ClassReader("data/TestInnerClass");
+        
+        JavaSourcer jw = new JavaSourcer(new Output(sw));
+        cr.accept(jw, 0);
+        
+        assertSourceEquals(
+                "package data;\n" + 
+                "public class TestInnerClass extends java.lang.Object {\n" + 
+                "    private final java.lang.String mArg;\n" + 
+                "    public TestInnerClass() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public TestInnerClass(java.lang.String arg0) {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public java.lang.String getArg() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public data.TestInnerClass$InnerPubClass getInnerPubClass() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "}",
+                sw.toString());
+    }
+
+    @Test
+    public void testTemplateClassSource() throws Exception {
+        StringWriter sw = new StringWriter();
+        ClassReader cr = new ClassReader("data/TestTemplateClass");
+        
+        JavaSourcer jw = new JavaSourcer(new Output(sw));
+        cr.accept(jw, 0);
+        
+        assertSourceEquals(
+                "package data;\n" + 
+                "public class TestTemplateClass<T extends java.io.InputStream, U extends java.lang.Object> extends java.lang.Object {\n" + 
+                "    private final java.util.Map<T, U> mMap_T_U;\n" + 
+                "    public java.util.Map<java.util.ArrayList<T>, java.util.Map<java.lang.String, java.util.ArrayList<U>>> mMap_T_S_U;\n" + 
+                "    public TestTemplateClass() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public java.util.Map<T, U> getMap_T_U() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public java.util.Map<java.util.ArrayList<T>, java.util.Map<java.lang.String, java.util.ArrayList<U>>> getMap_T_S_U() {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public void draw(java.util.List<? extends org.w3c.dom.css.Rect> arg0) {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public static <T extends java.lang.Comparable<? super T>> void sort(java.util.List<T> arg0) {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" + 
+                "    public <X extends T, Y extends java.lang.Object> void getMap(java.util.List<T> arg0, java.util.Map<T, U> arg1, java.util.Map<X, java.util.Set<? super Y>> arg2) {\n" + 
+                "        throw new RuntimeException(\"Stub\");\n" + 
+                "    }\n" +
+                "}",
+                sw.toString());
+    }
+    
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/MethodSourcerTest.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/MethodSourcerTest.java
new file mode 100644
index 0000000..ca3d2da
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/MethodSourcerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.Opcodes;
+
+import java.io.StringWriter;
+
+/**
+ * 
+ */
+public class MethodSourcerTest extends TestHelper {
+
+    private StringWriter mWriter;
+    private Output mOutput;
+
+    @Before
+    public void setUp() throws Exception {
+        mWriter = new StringWriter();
+        mOutput = new Output(mWriter);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mWriter = null;
+    }
+
+    @Test
+    public void testVoid() {
+        MethodSourcer m = new MethodSourcer(mOutput,
+                "foo", //classname
+                Opcodes.ACC_PUBLIC, //access
+                "testVoid", //name
+                "()V", //desc
+                null, //signature
+                null); //exception
+        m.visitEnd();
+        
+        assertSourceEquals(
+                "public void testVoid() { }",
+                mWriter.toString());
+    }
+    
+    @Test
+    public void testVoidThrow() {
+        MethodSourcer m = new MethodSourcer(mOutput,
+                "foo", //classname
+                Opcodes.ACC_PUBLIC, //access
+                "testVoid", //name
+                "()V", //desc
+                null, //signature
+                new String[] { "java/lang/Exception" }); //exception
+        m.visitEnd();
+        
+        assertSourceEquals(
+                "public void testVoid() throws java.lang.Exception { }",
+                mWriter.toString());
+    }
+    
+    @Test
+    public void testReturnMap() {
+        MethodSourcer m = new MethodSourcer(mOutput,
+                "foo", //classname
+                Opcodes.ACC_PUBLIC, //access
+                "getMap_T_U", //name
+                "()Ljava/util/Map;", //desc
+                "()Ljava/util/Map<TT;TU;>;", //signature
+                null); //exception
+        m.visitEnd();
+        
+        assertSourceEquals(
+                "public java.util.Map<T, U> getMap_T_U() { }",
+                mWriter.toString());
+    }
+
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/SignatureSourcerTest.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/SignatureSourcerTest.java
new file mode 100644
index 0000000..5d9f26f
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/SignatureSourcerTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.objectweb.asm.signature.SignatureReader;
+
+import java.util.ArrayList;
+
+/**
+ * 
+ */
+public class SignatureSourcerTest {
+
+    private SignatureSourcer mSourcer;
+
+    @Before
+    public void setUp() throws Exception {
+        mSourcer = new SignatureSourcer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testReturnMapNoArgs() {
+        SignatureReader reader = new SignatureReader(
+                "()Ljava/util/Map<Ljava/util/ArrayList<TT;>;Ljava/util/Map<Ljava/lang/String;Ljava/util/ArrayList<TU;>;>;>;");
+        reader.accept(mSourcer);
+        String result = mSourcer.getReturnType().toString();
+        
+        Assert.assertEquals(
+                "java.util.Map<java.util.ArrayList<T>, java.util.Map<java.lang.String, java.util.ArrayList<U>>>",
+                result);
+    }
+
+    @Test
+    public void testReturnVoid() {
+        SignatureReader reader = new SignatureReader(
+                "(Ljava/util/List<+Lorg/w3c/dom/css/Rect;>;)V");
+        reader.accept(mSourcer);
+        String result = mSourcer.getReturnType().toString();
+        
+        Assert.assertEquals(
+                "void",
+                result);
+    }
+
+    @Test
+    public void testSimpleArg() {
+        SignatureReader reader = new SignatureReader(
+                "(Ljava/util/List<+Lorg/w3c/dom/css/Rect;>;)V");
+        reader.accept(mSourcer);
+        
+        ArrayList<SignatureSourcer> params = mSourcer.getParameters();
+        Assert.assertNotNull(params);
+        
+        String[] array = toStringArray(params);
+
+        Assert.assertArrayEquals(
+                new String[] { "java.util.List<? extends org.w3c.dom.css.Rect>" },
+                array);
+    }
+
+    @Test
+    public void testFormalParameters1() {
+        SignatureReader reader = new SignatureReader("<X:TT;Y:Ljava/lang/Object;>()V");
+        reader.accept(mSourcer);
+
+        Assert.assertTrue(mSourcer.hasFormalsContent());
+        
+        String result = mSourcer.formalsToString();
+        Assert.assertEquals(
+                "<X extends T, Y extends java.lang.Object>",
+                result);
+    }
+
+    @Test
+    public void testFormalParameters2() {
+        SignatureReader reader = new SignatureReader("<T::Ljava/lang/Comparable<-TT;>;>(Ljava/util/List<TT;>;)V");
+        reader.accept(mSourcer);
+
+        Assert.assertTrue(mSourcer.hasFormalsContent());
+        
+        String result = mSourcer.formalsToString();
+        Assert.assertEquals(
+                "<T extends java.lang.Comparable<? super T>>",
+                result);
+    }
+    
+    @Test
+    public void testManyArgs() {
+        SignatureReader reader = new SignatureReader(
+                "<X:TT;Y:Ljava/lang/Object;>(Ljava/util/List<TT;>;Ljava/util/Map<TT;TU;>;Ljava/util/Map<TX;Ljava/util/Set<-TY;>;>;)V");
+        reader.accept(mSourcer);
+
+        Assert.assertTrue(mSourcer.hasFormalsContent());
+        String formals = mSourcer.formalsToString();
+        Assert.assertEquals(
+                "<X extends T, Y extends java.lang.Object>",
+                formals);
+
+        String result = mSourcer.getReturnType().toString();
+        Assert.assertEquals(
+                "void",
+                result);
+        
+        ArrayList<SignatureSourcer> params = mSourcer.getParameters();
+        Assert.assertNotNull(params);
+        
+        String[] array = toStringArray(params);
+
+        Assert.assertArrayEquals(
+                new String[] { "java.util.List<T>",
+                               "java.util.Map<T, U>",
+                               "java.util.Map<X, java.util.Set<? super Y>>" },
+                array);
+    }
+
+    private String[] toStringArray(ArrayList<?> params) {
+        String[] array = new String[params.size()];
+        for (int i = 0; i < params.size(); i++) {
+            array[i] = params.get(i).toString();
+        }
+        return array;
+    }
+}
diff --git a/tools/mkstubs/tests/com/android/mkstubs/sourcer/TestHelper.java b/tools/mkstubs/tests/com/android/mkstubs/sourcer/TestHelper.java
new file mode 100644
index 0000000..57bdee0
--- /dev/null
+++ b/tools/mkstubs/tests/com/android/mkstubs/sourcer/TestHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2009 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.mkstubs.sourcer;
+
+import org.junit.Assert;
+
+/**
+ * 
+ */
+abstract class TestHelper {
+
+    /**
+     * Test source equality after normalizing all whitespace.
+     */
+    public void assertSourceEquals(String expected, String actual) {
+        String en = expected.replaceAll("[\\s]+", " ").trim();
+        String an = actual.replaceAll(  "[\\s]+", " ").trim();
+        
+        Assert.assertEquals(
+                String.format("Source comparison failure: expected:<%s> but was:<%s>", expected, actual),
+                en, an);
+    }
+}
diff --git a/tools/mkstubs/tests/data/TestBaseClass.java b/tools/mkstubs/tests/data/TestBaseClass.java
new file mode 100644
index 0000000..2b1b438
--- /dev/null
+++ b/tools/mkstubs/tests/data/TestBaseClass.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2009 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 data;
+
+
+/**
+ * 
+ */
+public class TestBaseClass implements Runnable {
+
+    private final String mArg;
+    
+    public TestBaseClass() {
+        throw new RuntimeException("Stub");
+    }
+    
+    public TestBaseClass(String arg) {
+        mArg = arg;
+    }
+
+    public String getArg() {
+        return mArg;
+    }
+
+    @SuppressWarnings("unused")
+    public void run() {
+    }
+}
diff --git a/tools/mkstubs/tests/data/TestInnerClass.java b/tools/mkstubs/tests/data/TestInnerClass.java
new file mode 100644
index 0000000..e19b969
--- /dev/null
+++ b/tools/mkstubs/tests/data/TestInnerClass.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2009 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 data;
+
+
+/**
+ * 
+ */
+public class TestInnerClass {
+
+    private final String mArg;
+
+    private class InnerPrivClass {
+    }
+
+    public class InnerPubClass {
+    }
+
+    private static final class InnerStaticClass {
+    }
+    
+    public TestInnerClass() {
+        mArg = null;
+    }
+    
+    public TestInnerClass(String arg) {
+        mArg = arg;
+    }
+
+    public String getArg() {
+        return mArg;
+    }
+
+    public InnerPubClass getInnerPubClass() {
+        return new InnerPubClass();
+    }
+    
+}
diff --git a/tools/mkstubs/tests/data/TestTemplateClass.java b/tools/mkstubs/tests/data/TestTemplateClass.java
new file mode 100644
index 0000000..bcc04c0
--- /dev/null
+++ b/tools/mkstubs/tests/data/TestTemplateClass.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2009 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 data;
+
+import org.w3c.dom.css.Rect;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 
+ */
+public class TestTemplateClass<T extends InputStream, U> {
+
+    private final Map<T, U> mMap_T_U = null;
+    
+    public Map<ArrayList<T>, Map<String, ArrayList<U>>> mMap_T_S_U = null;
+    
+    public TestTemplateClass() {
+    }
+    
+    public Map<T, U> getMap_T_U() {
+        return mMap_T_U;
+    }
+    
+    public Map<ArrayList<T>, Map<String, ArrayList<U>>> getMap_T_S_U() {
+        return mMap_T_S_U;
+    }
+
+    public void draw(List<? extends Rect> shape) {
+    }
+
+    public static <T extends Comparable<? super T>> void sort(List<T> list) {
+    }
+
+    public <X extends T, Y> void getMap(List<T> list, Map<T, U> tu, Map<X, Set<? super Y>> xy) {
+    }
+}