Refactoring to support more annotation types.

Split the actual annotation handling logic out of AnnotationVisitor,
instead delegating to a new AnnotationHandler interface. The visitor now
simply takes a map of annotation name to handler, and delegate all the
interesting work.

Create a new GreylistAnnotationHandler for dealing with the existing
@UnsupportedAppUsage annotations.

Add support for writing to a whitelist file too, though this is not used
yet in this CL. A new parameter --write-whitelist specifies a filename to
write this to.

Also refactor the tests along similar lines.

Bug: 112186571
Test: atest class2greylisttest
Change-Id: Ic4da92f3499da7f40f9207ad1776ad79d76464a4
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
new file mode 100644
index 0000000..eb54a33
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
@@ -0,0 +1,66 @@
+package com.android.class2greylist;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.classfile.FieldOrMethod;
+import org.apache.bcel.classfile.JavaClass;
+
+import java.util.Locale;
+
+/**
+ * Encapsulates context for a single annotation on a class member.
+ */
+public class AnnotationContext {
+
+    public final Status status;
+    public final FieldOrMethod member;
+    public final JavaClass definingClass;
+    public final String signatureFormatString;
+
+    public AnnotationContext(
+            Status status,
+            FieldOrMethod member,
+            JavaClass definingClass,
+            String signatureFormatString) {
+        this.status = status;
+        this.member = member;
+        this.definingClass = definingClass;
+        this.signatureFormatString = signatureFormatString;
+    }
+
+    /**
+     * @return the full descriptor of enclosing class.
+     */
+    public String getClassDescriptor() {
+        // JavaClass.getName() returns the Java-style name (with . not /), so we must fetch
+        // the original class name from the constant pool.
+        return definingClass.getConstantPool().getConstantString(
+                definingClass.getClassNameIndex(), Const.CONSTANT_Class);
+    }
+
+    /**
+     * @return the full descriptor of this member, in the format expected in
+     * the greylist.
+     */
+    public String getMemberDescriptor() {
+        return String.format(Locale.US, signatureFormatString,
+                getClassDescriptor(), member.getName(), member.getSignature());
+    }
+
+    /**
+     * Report an error in this context. The final error message will include
+     * the class and member names, and the source file name.
+     */
+    public void reportError(String message, Object... args) {
+        StringBuilder error = new StringBuilder();
+        error.append(definingClass.getSourceFileName())
+                .append(": ")
+                .append(definingClass.getClassName())
+                .append(".")
+                .append(member.getName())
+                .append(": ")
+                .append(String.format(Locale.US, message, args));
+
+        status.error(error.toString());
+    }
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
new file mode 100644
index 0000000..92d2ab6
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
@@ -0,0 +1,11 @@
+package com.android.class2greylist;
+
+import org.apache.bcel.classfile.AnnotationEntry;
+
+/**
+ * Interface for an annotation handler, which handle individual annotations on
+ * class members.
+ */
+public interface AnnotationHandler {
+    void handleAnnotation(AnnotationEntry annotation, AnnotationContext context);
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
index 1838575..b805b30 100644
--- a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
@@ -16,100 +16,40 @@
 
 package com.android.class2greylist;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-
-import org.apache.bcel.Const;
 import org.apache.bcel.classfile.AnnotationEntry;
 import org.apache.bcel.classfile.DescendingVisitor;
-import org.apache.bcel.classfile.ElementValue;
-import org.apache.bcel.classfile.ElementValuePair;
 import org.apache.bcel.classfile.EmptyVisitor;
 import org.apache.bcel.classfile.Field;
 import org.apache.bcel.classfile.FieldOrMethod;
 import org.apache.bcel.classfile.JavaClass;
 import org.apache.bcel.classfile.Method;
-import org.apache.bcel.classfile.SimpleElementValue;
 
-import java.util.Locale;
-import java.util.Set;
-import java.util.function.Predicate;
+import java.util.Map;
 
 /**
- * Visits a JavaClass instance and pulls out all members annotated with a
- * specific annotation. The signatures of such members are passed to {@link
- * GreylistConsumer#greylistEntry(String, Integer)}. Any errors result in a
- * call to {@link Status#error(String, Object...)}.
- *
- * If the annotation has a property "expectedSignature" the generated signature
- * will be verified against the one specified there. If it differs, an error
- * will be generated.
+ * Visits a JavaClass instance and passes any annotated members to a {@link AnnotationHandler}
+ * according to the map provided.
  */
 public class AnnotationVisitor extends EmptyVisitor {
 
-    private static final String EXPECTED_SIGNATURE = "expectedSignature";
-    private static final String MAX_TARGET_SDK = "maxTargetSdk";
-
     private final JavaClass mClass;
-    private final String mAnnotationType;
-    private final Predicate<Member> mMemberFilter;
-    private final Set<Integer> mValidMaxTargetSdkValues;
-    private final GreylistConsumer mConsumer;
     private final Status mStatus;
     private final DescendingVisitor mDescendingVisitor;
+    private final Map<String, AnnotationHandler> mAnnotationHandlers;
 
     /**
-     * Represents a member of a class file (a field or method).
+     * Creates a visitor for a class.
+     *
+     * @param clazz Class to visit
+     * @param status For reporting debug information
+     * @param handlers Map of {@link AnnotationHandler}. The keys should be annotation names, as
+     *                 class descriptors.
      */
-    @VisibleForTesting
-    public static class Member {
-
-        /**
-         * Signature of this member.
-         */
-        public final String signature;
-        /**
-         * Indicates if this is a synthetic bridge method.
-         */
-        public final boolean bridge;
-        /**
-         * Max target SDK of property this member, if it is set, else null.
-         *
-         * Note: even though the annotation itself specified a default value,
-         * that default value is not encoded into instances of the annotation
-         * in class files. So when no value is specified in source, it will
-         * result in null appearing in here.
-         */
-        public final Integer maxTargetSdk;
-
-        public Member(String signature, boolean bridge, Integer maxTargetSdk) {
-            this.signature = signature;
-            this.bridge = bridge;
-            this.maxTargetSdk = maxTargetSdk;
-        }
-    }
-
-    public AnnotationVisitor(JavaClass clazz, String annotation, Set<String> publicApis,
-            Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer,
-            Status status) {
-        this(clazz,
-                annotation,
-                member -> !(member.bridge && publicApis.contains(member.signature)),
-                validMaxTargetSdkValues,
-                consumer,
-                status);
-    }
-
-    @VisibleForTesting
-    public AnnotationVisitor(JavaClass clazz, String annotation, Predicate<Member> memberFilter,
-            Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer,
-            Status status) {
+    public AnnotationVisitor(JavaClass clazz, Status status,
+            Map<String, AnnotationHandler> handlers) {
         mClass = clazz;
-        mAnnotationType = annotation;
-        mMemberFilter = memberFilter;
-        mValidMaxTargetSdkValues = validMaxTargetSdkValues;
-        mConsumer = consumer;
         mStatus = status;
+        mAnnotationHandlers = handlers;
         mDescendingVisitor = new DescendingVisitor(clazz, this);
     }
 
@@ -118,13 +58,6 @@
         mDescendingVisitor.visit();
     }
 
-    private static String getClassDescriptor(JavaClass clazz) {
-        // JavaClass.getName() returns the Java-style name (with . not /), so we must fetch
-        // the original class name from the constant pool.
-        return clazz.getConstantPool().getConstantString(
-                clazz.getClassNameIndex(), Const.CONSTANT_Class);
-    }
-
     @Override
     public void visitMethod(Method method) {
         visitMember(method, "L%s;->%s%s");
@@ -136,80 +69,15 @@
     }
 
     private void visitMember(FieldOrMethod member, String signatureFormatString) {
-        JavaClass definingClass = (JavaClass) mDescendingVisitor.predecessor();
         mStatus.debug("Visit member %s : %s", member.getName(), member.getSignature());
+        AnnotationContext context = new AnnotationContext(mStatus, member,
+                (JavaClass) mDescendingVisitor.predecessor(), signatureFormatString);
         for (AnnotationEntry a : member.getAnnotationEntries()) {
-            if (mAnnotationType.equals(a.getAnnotationType())) {
-                mStatus.debug("Member has annotation %s", mAnnotationType);
-                // For fields, the same access flag means volatile, so only check for methods.
-                boolean bridge = (member instanceof Method)
-                        && (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
-                if (bridge) {
-                    mStatus.debug("Member is a bridge", mAnnotationType);
-                }
-                String signature = String.format(Locale.US, signatureFormatString,
-                        getClassDescriptor(definingClass), member.getName(), member.getSignature());
-                Integer maxTargetSdk = null;
-                for (ElementValuePair property : a.getElementValuePairs()) {
-                    switch (property.getNameString()) {
-                        case EXPECTED_SIGNATURE:
-                            verifyExpectedSignature(
-                                    property, signature, definingClass, member, bridge);
-                            break;
-                        case MAX_TARGET_SDK:
-                            maxTargetSdk = verifyAndGetMaxTargetSdk(
-                                    property, definingClass, member);
-                            break;
-                    }
-                }
-                if (mMemberFilter.test(new Member(signature, bridge, maxTargetSdk))) {
-                    mConsumer.greylistEntry(signature, maxTargetSdk);
-                }
+            if (mAnnotationHandlers.containsKey(a.getAnnotationType())) {
+                mStatus.debug("Member has annotation %s for which we have a handler",
+                        a.getAnnotationType());
+                mAnnotationHandlers.get(a.getAnnotationType()).handleAnnotation(a, context);
             }
         }
     }
-
-    private void verifyExpectedSignature(ElementValuePair property, String signature,
-            JavaClass definingClass, FieldOrMethod member, boolean isBridge) {
-        String expected = property.getValue().stringifyValue();
-        // Don't enforce for bridge methods; they're generated so won't match.
-        if (!isBridge && !signature.equals(expected)) {
-            error(definingClass, member,
-                    "Expected signature does not match generated:\n"
-                            + "Expected:  %s\n"
-                            + "Generated: %s", expected, signature);
-        }
-    }
-
-    private Integer verifyAndGetMaxTargetSdk(
-            ElementValuePair property, JavaClass definingClass, FieldOrMethod member) {
-        if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) {
-            error(definingClass, member, "Expected property %s to be of type int; got %d",
-                    property.getNameString(), property.getValue().getElementValueType());
-        }
-        int value = ((SimpleElementValue) property.getValue()).getValueInt();
-        if (!mValidMaxTargetSdkValues.contains(value)) {
-            error(definingClass, member,
-                    "Invalid value for %s: got %d, expected one of [%s]",
-                    property.getNameString(),
-                    value,
-                    Joiner.on(",").join(mValidMaxTargetSdkValues));
-            return null;
-        }
-        return value;
-    }
-
-    private void error(JavaClass clazz, FieldOrMethod member, String message, Object... args) {
-        StringBuilder error = new StringBuilder();
-        error.append(clazz.getSourceFileName())
-                .append(": ")
-                .append(clazz.getClassName())
-                .append(".")
-                .append(member.getName())
-                .append(": ")
-                .append(String.format(Locale.US, message, args));
-
-        mStatus.error(error.toString());
-    }
-
 }
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 64a0357..c0a3160 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -17,6 +17,8 @@
 package com.android.class2greylist;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.io.Files;
 
@@ -44,12 +46,18 @@
  */
 public class Class2Greylist {
 
-    private static final String ANNOTATION_TYPE = "Landroid/annotation/UnsupportedAppUsage;";
+    private static final String GREYLIST_ANNOTATION = "Landroid/annotation/UnsupportedAppUsage;";
+    private static final Set<String> WHITELIST_ANNOTATIONS = ImmutableSet.of();
 
     private final Status mStatus;
     private final String mPublicApiListFile;
     private final String[] mPerSdkOutputFiles;
+    private final String mWhitelistFile;
     private final String[] mJarFiles;
+    private final GreylistConsumer mOutput;
+    private final Set<Integer> mAllowedSdkVersions;
+    private final Set<String> mPublicApis;
+
 
     public static void main(String[] args) {
         Options options = new Options();
@@ -68,6 +76,11 @@
                         "no integer is given, members with no maxTargetSdk are written.")
                 .create("g"));
         options.addOption(OptionBuilder
+                .withLongOpt("write-whitelist")
+                .hasArgs(1)
+                .withDescription("Specify file to write whitelist to.")
+                .create('w'));
+        options.addOption(OptionBuilder
                 .withLongOpt("debug")
                 .hasArgs(0)
                 .withDescription("Enable debug")
@@ -100,9 +113,13 @@
         }
 
         Status status = new Status(cmd.hasOption('d'));
-        Class2Greylist c2gl = new Class2Greylist(
-                status, cmd.getOptionValue('p', null), cmd.getOptionValues('g'), jarFiles);
         try {
+            Class2Greylist c2gl = new Class2Greylist(
+                    status,
+                    cmd.getOptionValue('p', null),
+                    cmd.getOptionValues('g'),
+                    cmd.getOptionValue('w', null),
+                    jarFiles);
             c2gl.main();
         } catch (IOException e) {
             status.error(e);
@@ -118,52 +135,58 @@
 
     @VisibleForTesting
     Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles,
-            String[] jarFiles) {
+            String whitelistOutputFile, String[] jarFiles) throws IOException {
         mStatus = status;
         mPublicApiListFile = publicApiListFile;
         mPerSdkOutputFiles = perSdkLevelOutputFiles;
+        mWhitelistFile = whitelistOutputFile;
         mJarFiles = jarFiles;
-    }
-
-    private void main() throws IOException {
-        GreylistConsumer output;
-        Set<Integer> allowedSdkVersions;
         if (mPerSdkOutputFiles != null) {
-            Map<Integer, String> outputFiles = readGreylistMap(mPerSdkOutputFiles);
-            output = new FileWritingGreylistConsumer(mStatus, outputFiles);
-            allowedSdkVersions = outputFiles.keySet();
+            Map<Integer, String> outputFiles = readGreylistMap(mStatus, mPerSdkOutputFiles);
+            mOutput = new FileWritingGreylistConsumer(mStatus, outputFiles, mWhitelistFile);
+            mAllowedSdkVersions = outputFiles.keySet();
         } else {
             // TODO remove this once per-SDK greylist support integrated into the build.
             // Right now, mPerSdkOutputFiles is always null as the build never passes the
             // corresponding command lind flags. Once the build is updated, can remove this.
-            output = new SystemOutGreylistConsumer();
-            allowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28));
+            mOutput = new SystemOutGreylistConsumer();
+            mAllowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28));
         }
 
-        Set<String> publicApis;
         if (mPublicApiListFile != null) {
-            publicApis = Sets.newHashSet(
+            mPublicApis = Sets.newHashSet(
                     Files.readLines(new File(mPublicApiListFile), Charset.forName("UTF-8")));
         } else {
-            publicApis = Collections.emptySet();
+            mPublicApis = Collections.emptySet();
         }
+    }
 
+    private Map<String, AnnotationHandler> createAnnotationHandlers() {
+        return ImmutableMap.<String, AnnotationHandler>builder()
+                .put(GreylistAnnotationHandler.ANNOTATION_NAME,
+                        new GreylistAnnotationHandler(
+                                mStatus, mOutput, mPublicApis, mAllowedSdkVersions))
+                .build();
+    }
+
+    private void main() throws IOException {
+        Map<String, AnnotationHandler> handlers = createAnnotationHandlers();
         for (String jarFile : mJarFiles) {
             mStatus.debug("Processing jar file %s", jarFile);
             try {
                 JarReader reader = new JarReader(mStatus, jarFile);
-                reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, ANNOTATION_TYPE,
-                        publicApis, allowedSdkVersions, output, mStatus).visit());
+                reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, mStatus, handlers)
+                        .visit());
                 reader.close();
             } catch (IOException e) {
                 mStatus.error(e);
             }
         }
-        output.close();
+        mOutput.close();
     }
 
     @VisibleForTesting
-    Map<Integer, String> readGreylistMap(String[] argValues) {
+    static Map<Integer, String> readGreylistMap(Status status, String[] argValues) {
         Map<Integer, String> map = new HashMap<>();
         for (String sdkFile : argValues) {
             Integer maxTargetSdk = null;
@@ -173,12 +196,12 @@
                 try {
                     maxTargetSdk = Integer.valueOf(sdkFile.substring(0, colonPos));
                 } catch (NumberFormatException nfe) {
-                    mStatus.error("Not a valid integer: %s from argument value '%s'",
+                    status.error("Not a valid integer: %s from argument value '%s'",
                             sdkFile.substring(0, colonPos), sdkFile);
                 }
                 filename = sdkFile.substring(colonPos + 1);
                 if (filename.length() == 0) {
-                    mStatus.error("Not a valid file name: %s from argument value '%s'",
+                    status.error("Not a valid file name: %s from argument value '%s'",
                             filename, sdkFile);
                 }
             } else {
@@ -186,7 +209,7 @@
                 filename = sdkFile;
             }
             if (map.containsKey(maxTargetSdk)) {
-                mStatus.error("Multiple output files for maxTargetSdk %s", maxTargetSdk);
+                status.error("Multiple output files for maxTargetSdk %s", maxTargetSdk);
             } else {
                 map.put(maxTargetSdk, filename);
             }
diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
index 86eeeff..9f33467 100644
--- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
@@ -11,21 +11,29 @@
 
     private final Status mStatus;
     private final Map<Integer, PrintStream> mSdkToPrintStreamMap;
+    private final PrintStream mWhitelistStream;
+
+    private static PrintStream openFile(String filename) throws FileNotFoundException {
+        if (filename == null) {
+            return null;
+        }
+        return new PrintStream(new FileOutputStream(new File(filename)));
+    }
 
     private static Map<Integer, PrintStream> openFiles(
             Map<Integer, String> filenames) throws FileNotFoundException {
         Map<Integer, PrintStream> streams = new HashMap<>();
         for (Map.Entry<Integer, String> entry : filenames.entrySet()) {
-            streams.put(entry.getKey(),
-                    new PrintStream(new FileOutputStream(new File(entry.getValue()))));
+            streams.put(entry.getKey(), openFile(entry.getValue()));
         }
         return streams;
     }
 
-    public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap)
-            throws FileNotFoundException {
+    public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap,
+            String whitelistFile) throws FileNotFoundException {
         mStatus = status;
         mSdkToPrintStreamMap = openFiles(sdkToFilenameMap);
+        mWhitelistStream = openFile(whitelistFile);
     }
 
     @Override
@@ -40,9 +48,19 @@
     }
 
     @Override
+    public void whitelistEntry(String signature) {
+        if (mWhitelistStream != null) {
+            mWhitelistStream.println(signature);
+        }
+    }
+
+    @Override
     public void close() {
         for (PrintStream p : mSdkToPrintStreamMap.values()) {
             p.close();
         }
+        if (mWhitelistStream != null) {
+            mWhitelistStream.close();
+        }
     }
 }
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
new file mode 100644
index 0000000..460f2c3
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
@@ -0,0 +1,146 @@
+package com.android.class2greylist;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.ElementValue;
+import org.apache.bcel.classfile.ElementValuePair;
+import org.apache.bcel.classfile.FieldOrMethod;
+import org.apache.bcel.classfile.Method;
+import org.apache.bcel.classfile.SimpleElementValue;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Processes {@code UnsupportedAppUsage} annotations to generate greylist
+ * entries.
+ *
+ * Any annotations with a {@link #EXPECTED_SIGNATURE} property will have their
+ * generated signature verified against this, and an error will be reported if
+ * it does not match. Exclusions are made for bridge methods.
+ *
+ * Any {@link #MAX_TARGET_SDK} properties will be validated against the given
+ * set of valid values, then passed through to the greylist consumer.
+ */
+public class GreylistAnnotationHandler implements AnnotationHandler {
+
+    public static final String ANNOTATION_NAME = "Landroid/annotation/UnsupportedAppUsage;";
+
+    // properties of greylist annotations:
+    private static final String EXPECTED_SIGNATURE = "expectedSignature";
+    private static final String MAX_TARGET_SDK = "maxTargetSdk";
+
+    private final Status mStatus;
+    private final Predicate<GreylistMember> mGreylistFilter;
+    private final GreylistConsumer mGreylistConsumer;
+    private final Set<Integer> mValidMaxTargetSdkValues;
+
+    /**
+     * Represents a member of a class file (a field or method).
+     */
+    @VisibleForTesting
+    public static class GreylistMember {
+
+        /**
+         * Signature of this member.
+         */
+        public final String signature;
+        /**
+         * Indicates if this is a synthetic bridge method.
+         */
+        public final boolean bridge;
+        /**
+         * Max target SDK of property this member, if it is set, else null.
+         *
+         * Note: even though the annotation itself specified a default value,
+         * that default value is not encoded into instances of the annotation
+         * in class files. So when no value is specified in source, it will
+         * result in null appearing in here.
+         */
+        public final Integer maxTargetSdk;
+
+        public GreylistMember(String signature, boolean bridge, Integer maxTargetSdk) {
+            this.signature = signature;
+            this.bridge = bridge;
+            this.maxTargetSdk = maxTargetSdk;
+        }
+    }
+
+    public GreylistAnnotationHandler(
+            Status status,
+            GreylistConsumer greylistConsumer,
+            Set<String> publicApis,
+            Set<Integer> validMaxTargetSdkValues) {
+        this(status, greylistConsumer,
+                member -> !(member.bridge && publicApis.contains(member.signature)),
+                validMaxTargetSdkValues);
+    }
+
+    @VisibleForTesting
+    public GreylistAnnotationHandler(
+            Status status,
+            GreylistConsumer greylistConsumer,
+            Predicate<GreylistMember> greylistFilter,
+            Set<Integer> validMaxTargetSdkValues) {
+        mStatus = status;
+        mGreylistConsumer = greylistConsumer;
+        mGreylistFilter = greylistFilter;
+        mValidMaxTargetSdkValues = validMaxTargetSdkValues;
+    }
+
+    @Override
+    public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) {
+        FieldOrMethod member = context.member;
+        boolean bridge = (member instanceof Method)
+                && (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
+        if (bridge) {
+            mStatus.debug("Member is a bridge");
+        }
+        String signature = context.getMemberDescriptor();
+        Integer maxTargetSdk = null;
+        for (ElementValuePair property : annotation.getElementValuePairs()) {
+            switch (property.getNameString()) {
+                case EXPECTED_SIGNATURE:
+                    verifyExpectedSignature(context, property, signature, bridge);
+                    break;
+                case MAX_TARGET_SDK:
+                    maxTargetSdk = verifyAndGetMaxTargetSdk(context, property);
+                    break;
+            }
+        }
+        if (mGreylistFilter.test(new GreylistMember(signature, bridge, maxTargetSdk))) {
+            mGreylistConsumer.greylistEntry(signature, maxTargetSdk);
+        }
+    }
+
+    private void verifyExpectedSignature(AnnotationContext context, ElementValuePair property,
+            String signature, boolean isBridge) {
+        String expected = property.getValue().stringifyValue();
+        // Don't enforce for bridge methods; they're generated so won't match.
+        if (!isBridge && !signature.equals(expected)) {
+            context.reportError("Expected signature does not match generated:\n"
+                            + "Expected:  %s\n"
+                            + "Generated: %s", expected, signature);
+        }
+    }
+
+    private Integer verifyAndGetMaxTargetSdk(AnnotationContext context, ElementValuePair property) {
+        if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) {
+            context.reportError("Expected property %s to be of type int; got %d",
+                    property.getNameString(), property.getValue().getElementValueType());
+        }
+        int value = ((SimpleElementValue) property.getValue()).getValueInt();
+        if (!mValidMaxTargetSdkValues.contains(value)) {
+            context.reportError("Invalid value for %s: got %d, expected one of [%s]",
+                    property.getNameString(),
+                    value,
+                    Joiner.on(",").join(mValidMaxTargetSdkValues));
+            return null;
+        }
+        return value;
+    }
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
index debc21d..fd855e8 100644
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
@@ -9,5 +9,12 @@
      */
     void greylistEntry(String signature, Integer maxTargetSdk);
 
+    /**
+     * Handle a new whitelist entry.
+     *
+     * @param signature Signature of the member.
+     */
+    void whitelistEntry(String signature);
+
     void close();
 }
diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
index 8e12759..ad5ad70 100644
--- a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
@@ -7,6 +7,12 @@
     }
 
     @Override
+    public void whitelistEntry(String signature) {
+        // Ignore. This class is only used when no grey/white lists are
+        // specified, so we have nowhere to write whitelist entries.
+    }
+
+    @Override
     public void close() {
     }
 }
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
new file mode 100644
index 0000000..8f4a76f
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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.class2greylist;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
+
+import com.android.javac.Javac;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+import java.io.IOException;
+
+public class AnnotationHandlerTestBase {
+
+    @Rule
+    public TestName mTestName = new TestName();
+
+    protected Javac mJavac;
+    protected GreylistConsumer mConsumer;
+    protected Status mStatus;
+
+    @Before
+    public void baseSetup() throws IOException {
+        System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
+                mTestName.getMethodName()));
+        mConsumer = mock(GreylistConsumer.class);
+        mStatus = mock(Status.class, withSettings().verboseLogging());
+        mJavac = new Javac();
+    }
+
+    protected void assertNoErrors() {
+        verify(mStatus, never()).error(any(Throwable.class));
+        verify(mStatus, never()).error(any(), any());
+    }
+}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
index 979044b..cb75dd3 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2018 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.class2greylist;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -32,41 +48,36 @@
     }
 
     @Test
-    public void testReadGreylistMap() {
-        Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
-        Map<Integer, String> map = c2gl.readGreylistMap(
+    public void testReadGreylistMap() throws IOException {
+        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
                 new String[]{"noApi", "1:apiOne", "3:apiThree"});
         verifyZeroInteractions(mStatus);
         assertThat(map).containsExactly(null, "noApi", 1, "apiOne", 3, "apiThree");
     }
 
     @Test
-    public void testReadGreylistMapDuplicate() {
-        Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
-        Map<Integer, String> map = c2gl.readGreylistMap(
+    public void testReadGreylistMapDuplicate() throws IOException {
+        Class2Greylist.readGreylistMap(mStatus,
                 new String[]{"noApi", "1:apiOne", "1:anotherOne"});
         verify(mStatus, atLeastOnce()).error(any(), any());
     }
 
     @Test
     public void testReadGreylistMapDuplicateNoApi() {
-        Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
-        Map<Integer, String> map = c2gl.readGreylistMap(
+        Class2Greylist.readGreylistMap(mStatus,
                 new String[]{"noApi", "anotherNoApi", "1:apiOne"});
         verify(mStatus, atLeastOnce()).error(any(), any());
     }
 
     @Test
-    public void testReadGreylistMapInvalidInt() {
-        Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
-        Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "a:apiOne"});
+    public void testReadGreylistMapInvalidInt() throws IOException {
+        Class2Greylist.readGreylistMap(mStatus, new String[]{"noApi", "a:apiOne"});
         verify(mStatus, atLeastOnce()).error(any(), any());
     }
 
     @Test
-    public void testReadGreylistMapNoFilename() {
-        Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
-        Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "1:"});
+    public void testReadGreylistMapNoFilename() throws IOException {
+        Class2Greylist.readGreylistMap(mStatus, new String[]{"noApi", "1:"});
         verify(mStatus, atLeastOnce()).error(any(), any());
     }
 }
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
similarity index 80%
rename from tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java
rename to tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
index 994fe89..1a4bfb8 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
@@ -19,47 +19,32 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.withSettings;
 
 import static java.util.Collections.emptySet;
 
-import com.android.javac.Javac;
-
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TestName;
 import org.mockito.ArgumentCaptor;
 
 import java.io.IOException;
+import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 
-public class AnnotationVisitorTest {
+public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
 
     private static final String ANNOTATION = "Lannotation/Anno;";
 
-    @Rule
-    public TestName mTestName = new TestName();
-
-    private Javac mJavac;
-    private GreylistConsumer mConsumer;
-    private Status mStatus;
-
     @Before
     public void setup() throws IOException {
-        System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
-                mTestName.getMethodName()));
-        mConsumer = mock(GreylistConsumer.class);
-        mStatus = mock(Status.class, withSettings().verboseLogging());
-        mJavac = new Javac();
         mJavac.addSource("annotation.Anno", Joiner.on('\n').join(
                 "package annotation;",
                 "import static java.lang.annotation.RetentionPolicy.CLASS;",
@@ -71,9 +56,11 @@
                 "}"));
     }
 
-    private void assertNoErrors() {
-        verify(mStatus, never()).error(any(Throwable.class));
-        verify(mStatus, never()).error(any(), any());
+    private GreylistAnnotationHandler createGreylistHandler(
+            Predicate<GreylistAnnotationHandler.GreylistMember> greylistFilter,
+            Set<Integer> validMaxTargetSdkValues) {
+        return new GreylistAnnotationHandler(
+                mStatus, mConsumer, greylistFilter, validMaxTargetSdkValues);
     }
 
     @Test
@@ -87,8 +74,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -107,8 +95,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -127,8 +116,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -147,8 +137,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -167,8 +158,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         verify(mStatus, times(1)).error(any(), any());
     }
@@ -186,8 +178,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -204,8 +197,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         verify(mConsumer, never()).greylistEntry(any(String.class), any());
@@ -222,8 +216,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+        ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -249,10 +244,10 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -281,10 +276,10 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -317,15 +312,12 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(
-                mJavac.getCompiledClass("a.b.Interface"), ANNOTATION, x -> true, emptySet(),
-                mConsumer, mStatus).visit();
-        new AnnotationVisitor(
-                mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, emptySet(), mConsumer,
-                mStatus).visit();
-        new AnnotationVisitor(
-                mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, emptySet(), mConsumer,
-                mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Interface"), mStatus, handlerMap)
+                .visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -357,10 +349,15 @@
         Set<String> publicApis = Sets.newHashSet(
                 "La/b/Base;->method(Ljava/lang/Object;)V",
                 "La/b/Class;->method(Ljava/lang/Object;)V");
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, publicApis,
-                emptySet(), mConsumer, mStatus).visit();
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, publicApis,
-                emptySet(), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION,
+                        new GreylistAnnotationHandler(
+                                mStatus,
+                                mConsumer,
+                                publicApis,
+                                emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -380,9 +377,11 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION,
-                member -> !member.bridge, // exclude bridge methods
-                emptySet(), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(
+                        member -> !member.bridge, // exclude bridge methods
+                        emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
         verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
@@ -400,8 +399,9 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                emptySet(), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         verify(mStatus, times(1)).error(any(), any());
     }
 
@@ -416,8 +416,11 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                ImmutableSet.of(1), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(
+                        x -> true,
+                        ImmutableSet.of(1)));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         assertNoErrors();
         ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
         verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
@@ -435,8 +438,11 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                ImmutableSet.of(1), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(
+                        x -> true,
+                        ImmutableSet.of(1)));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         assertNoErrors();
         ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
         verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
@@ -454,8 +460,12 @@
                 "}"));
         assertThat(mJavac.compile()).isTrue();
 
-        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
-                ImmutableSet.of(1), mConsumer, mStatus).visit();
+        Map<String, AnnotationHandler> handlerMap =
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(
+                        x -> true,
+                        ImmutableSet.of(1)));
+        new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         verify(mStatus, times(1)).error(any(), any());
     }
+
 }