Support CSV hiddenapi flags in class2greylist

Access flags for hiddenapi are being converted from one file per flag
to a single CSV file in the format:

  <api signature>,<flag1>,...,<flagN>

Adjust `class2greylist` to produce such a CSV instead of text files.

Test: m, phone boots
Test: atest art/tools/class2greylist/test
Change-Id: I6592027256e42d87589512ada1a8ce4bd1c03e63
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationConsumer.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationConsumer.java
new file mode 100644
index 0000000..0f5f413
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationConsumer.java
@@ -0,0 +1,18 @@
+package com.android.class2greylist;
+
+import java.util.Map;
+import java.util.Set;
+
+public interface AnnotationConsumer {
+    /**
+     * Handle a parsed annotation for a class member.
+     *
+     * @param apiSignature Signature of the class member.
+     * @param annotationProperties Map of stringified properties of this annotation.
+     * @param parsedFlags Array of flags parsed from the annotation for this member.
+     */
+    public void consume(String apiSignature, Map<String, String> annotationProperties,
+            Set<String> parsedFlags);
+
+    public void close();
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
index 92d2ab6..ba1f583 100644
--- a/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
@@ -1,11 +1,27 @@
 package com.android.class2greylist;
 
+import java.util.Map;
+import java.util.HashMap;
+
 import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.ElementValuePair;
+
 
 /**
- * Interface for an annotation handler, which handle individual annotations on
+ * Base class for an annotation handler, which handle individual annotations on
  * class members.
  */
-public interface AnnotationHandler {
-    void handleAnnotation(AnnotationEntry annotation, AnnotationContext context);
+public abstract class AnnotationHandler {
+    abstract void handleAnnotation(AnnotationEntry annotation, AnnotationContext context);
+
+    protected Map<String, String> stringifyAnnotationProperties(AnnotationEntry annotation) {
+        Map<String, String> content = new HashMap<String, String>();
+
+        // Stringify all annotation properties.
+        for (ElementValuePair prop : annotation.getElementValuePairs()) {
+            content.put(prop.getNameString(), prop.getValue().stringifyValue());
+        }
+
+        return content;
+    }
 }
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationPropertyWriter.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationPropertyWriter.java
new file mode 100644
index 0000000..aacd963
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationPropertyWriter.java
@@ -0,0 +1,56 @@
+package com.android.class2greylist;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class AnnotationPropertyWriter implements AnnotationConsumer {
+
+    private final PrintStream mOutput;
+    private final List<Map<String, String>> mContents;
+    private final Set<String> mColumns;
+
+    public AnnotationPropertyWriter(String csvFile) throws FileNotFoundException {
+        mOutput = new PrintStream(new FileOutputStream(new File(csvFile)));
+        mContents = new ArrayList<>();
+        mColumns = new HashSet<>();
+    }
+
+    public void consume(String apiSignature, Map<String, String> annotationProperties,
+            Set<String> parsedFlags) {
+        // Clone properties map.
+        Map<String, String> contents = new HashMap(annotationProperties);
+
+        // Append the member signature.
+        contents.put("signature", apiSignature);
+
+        // Store data.
+        mColumns.addAll(contents.keySet());
+        mContents.add(contents);
+    }
+
+    public void close() {
+        // Sort columns by name and print header row.
+        List<String> columns = new ArrayList<>(mColumns);
+        columns.sort(Comparator.naturalOrder());
+        mOutput.println(columns.stream().collect(Collectors.joining(",")));
+
+        // Sort contents according to columns and print.
+        for (Map<String, String> row : mContents) {
+            mOutput.println(columns.stream().map(column -> row.getOrDefault(column, ""))
+                    .collect(Collectors.joining(",")));
+        }
+
+        // Close output.
+        mOutput.close();
+    }
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 870f85a..621ee11 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -42,6 +42,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.HashMap;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -57,17 +58,28 @@
                     "Ldalvik/annotation/compat/UnsupportedAppUsage;");
     private static final Set<String> WHITELIST_ANNOTATIONS = ImmutableSet.of();
 
+    public static final String FLAG_WHITELIST = "whitelist";
+    public static final String FLAG_GREYLIST = "greylist";
+    public static final String FLAG_BLACKLIST = "blacklist";
+    public static final String FLAG_GREYLIST_MAX_O = "greylist-max-o";
+
+    private static final Map<Integer, String> TARGET_SDK_TO_LIST_MAP;
+    static {
+        Map<Integer, String> map = new HashMap<>();
+        map.put(null, FLAG_GREYLIST);
+        map.put(26, FLAG_GREYLIST_MAX_O);
+        map.put(28, FLAG_GREYLIST);
+        TARGET_SDK_TO_LIST_MAP = Collections.unmodifiableMap(map);
+    }
+
     private final Status mStatus;
     private final String mPublicApiListFile;
-    private final String[] mPerSdkOutputFiles;
-    private final String mWhitelistFile;
+    private final String mCsvFlagsFile;
     private final String mCsvMetadataFile;
     private final String[] mJarFiles;
-    private final GreylistConsumer mOutput;
-    private final Predicate<Integer> mAllowedSdkVersions;
+    private final AnnotationConsumer mOutput;
     private final Set<String> mPublicApis;
 
-
     public static void main(String[] args) {
         Options options = new Options();
         options.addOption(OptionBuilder
@@ -76,19 +88,9 @@
                 .withDescription("Public API list file. Used to de-dupe bridge methods.")
                 .create("p"));
         options.addOption(OptionBuilder
-                .withLongOpt("write-greylist")
-                .hasArgs()
-                .withDescription(
-                        "Specify file to write greylist to. Can be specified multiple times. " +
-                        "Format is either just a filename, or \"int[,int,...]:filename\". If " +
-                        "integers are given, members with matching maxTargetSdk values are " +
-                        "written to the file; if no integer or \"none\" is given, members with " +
-                        "no maxTargetSdk are written.")
-                .create("g"));
-        options.addOption(OptionBuilder
-                .withLongOpt("write-whitelist")
+                .withLongOpt("write-flags-csv")
                 .hasArgs(1)
-                .withDescription("Specify file to write whitelist to.")
+                .withDescription("Specify file to write hiddenapi flags to.")
                 .create('w'));
         options.addOption(OptionBuilder
                 .withLongOpt("debug")
@@ -106,7 +108,7 @@
                 .hasArgs(1)
                 .withDescription("Specify a file to write API metaadata to. This is a CSV file " +
                         "containing any annotation properties for all members. Do not use in " +
-                        "conjunction with --write-greylist or --write-whitelist.")
+                        "conjunction with --write-flags-csv.")
                 .create('c'));
         options.addOption(OptionBuilder
                 .withLongOpt("help")
@@ -144,7 +146,6 @@
                 Class2Greylist c2gl = new Class2Greylist(
                         status,
                         cmd.getOptionValue('p', null),
-                        cmd.getOptionValues('g'),
                         cmd.getOptionValue('w', null),
                         cmd.getOptionValue('c', null),
                         jarFiles);
@@ -163,34 +164,18 @@
     }
 
     @VisibleForTesting
-    Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles,
-            String whitelistOutputFile, String csvMetadataFile, String[] jarFiles)
+    Class2Greylist(Status status, String publicApiListFile, String csvFlagsFile,
+            String csvMetadataFile, String[] jarFiles)
             throws IOException {
         mStatus = status;
         mPublicApiListFile = publicApiListFile;
-        mPerSdkOutputFiles = perSdkLevelOutputFiles;
-        mWhitelistFile = whitelistOutputFile;
+        mCsvFlagsFile = csvFlagsFile;
         mCsvMetadataFile = csvMetadataFile;
         mJarFiles = jarFiles;
         if (mCsvMetadataFile != null) {
-            mOutput = new CsvGreylistConsumer(mStatus, mCsvMetadataFile);
-            mAllowedSdkVersions = x -> true;
+            mOutput = new AnnotationPropertyWriter(mCsvMetadataFile);
         } else {
-            Map<Integer, String> outputFiles = readGreylistMap(mStatus, mPerSdkOutputFiles);
-            mOutput = new FileWritingGreylistConsumer(mStatus, outputFiles, mWhitelistFile);
-            mAllowedSdkVersions = new Predicate<Integer>(){
-                @Override
-                public boolean test(Integer i) {
-                    return outputFiles.keySet().contains(i);
-                }
-
-                @Override
-                public String toString() {
-                    // we reply on this toString behaviour for readable error messages in
-                    // GreylistAnnotationHandler
-                    return Joiner.on(",").join(outputFiles.keySet());
-                }
-            };
+            mOutput = new HiddenapiFlagsWriter(mCsvFlagsFile);
         }
 
         if (mPublicApiListFile != null) {
@@ -203,14 +188,15 @@
 
     private Map<String, AnnotationHandler> createAnnotationHandlers() {
         Builder<String, AnnotationHandler> builder = ImmutableMap.builder();
-        GreylistAnnotationHandler greylistAnnotationHandler = new GreylistAnnotationHandler(
-            mStatus, mOutput, mPublicApis, mAllowedSdkVersions);
+        UnsupportedAppUsageAnnotationHandler greylistAnnotationHandler =
+                new UnsupportedAppUsageAnnotationHandler(
+                    mStatus, mOutput, mPublicApis, TARGET_SDK_TO_LIST_MAP);
         GREYLIST_ANNOTATIONS.forEach(a -> builder.put(a, greylistAnnotationHandler));
         return builder
                 .put(CovariantReturnTypeHandler.ANNOTATION_NAME,
-                        new CovariantReturnTypeHandler(mOutput, mPublicApis))
+                        new CovariantReturnTypeHandler(mOutput, mPublicApis, FLAG_WHITELIST))
                 .put(CovariantReturnTypeMultiHandler.ANNOTATION_NAME,
-                        new CovariantReturnTypeMultiHandler(mOutput, mPublicApis))
+                        new CovariantReturnTypeMultiHandler(mOutput, mPublicApis, FLAG_WHITELIST))
                 .build();
     }
 
@@ -230,48 +216,6 @@
         mOutput.close();
     }
 
-    @VisibleForTesting
-    static Map<Integer, String> readGreylistMap(Status status, String[] argValues) {
-        Map<Integer, String> map = new HashMap<>();
-        for (String sdkFile : argValues) {
-            List<Integer> maxTargetSdks = new ArrayList<>();
-            String filename;
-            int colonPos = sdkFile.indexOf(':');
-            if (colonPos != -1) {
-                String[] targets = sdkFile.substring(0, colonPos).split(",");
-                for (String target : targets) {
-                    if ("none".equals(target)) {
-                        maxTargetSdks.add(null);
-                    } else {
-                        try {
-                            maxTargetSdks.add(Integer.valueOf(target));
-                        } catch (NumberFormatException nfe) {
-                            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) {
-                    status.error("Not a valid file name: %s from argument value '%s'",
-                            filename, sdkFile);
-                }
-            } else {
-                maxTargetSdks.add(null);
-                filename = sdkFile;
-            }
-            for (Integer maxTargetSdk : maxTargetSdks) {
-                if (map.containsKey(maxTargetSdk)) {
-                    status.error("Multiple output files for maxTargetSdk %s",
-                            maxTargetSdk == null ? "none" : maxTargetSdk);
-                } else {
-                    map.put(maxTargetSdk, filename);
-                }
-            }
-        }
-        return map;
-    }
-
     private static void dumpAllMembers(Status status, String[] jarFiles) {
         for (String jarFile : jarFiles) {
             status.debug("Processing jar file %s", jarFile);
diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
index afd15b4..b8de7e9 100644
--- a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeHandler.java
@@ -1,12 +1,18 @@
 package com.android.class2greylist;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
 
 import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.Constant;
+import org.apache.bcel.classfile.ConstantPool;
+import org.apache.bcel.classfile.ElementValue;
 import org.apache.bcel.classfile.ElementValuePair;
 import org.apache.bcel.classfile.Method;
 
+import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -20,19 +26,22 @@
  * <p>Methods are also validated against the public API list, to assert that
  * the annotated method is already a public API.
  */
-public class CovariantReturnTypeHandler implements AnnotationHandler {
+public class CovariantReturnTypeHandler extends AnnotationHandler {
 
     private static final String SHORT_NAME = "CovariantReturnType";
     public static final String ANNOTATION_NAME = "Ldalvik/annotation/codegen/CovariantReturnType;";
 
     private static final String RETURN_TYPE = "returnType";
 
-    private final GreylistConsumer mConsumer;
+    private final AnnotationConsumer mAnnotationConsumer;
     private final Set<String> mPublicApis;
+    private final String mHiddenapiFlag;
 
-    public CovariantReturnTypeHandler(GreylistConsumer consumer, Set<String> publicApis) {
-        mConsumer = consumer;
+    public CovariantReturnTypeHandler(AnnotationConsumer consumer, Set<String> publicApis,
+            String hiddenapiFlag) {
+        mAnnotationConsumer = consumer;
         mPublicApis = publicApis;
+        mHiddenapiFlag = hiddenapiFlag;
     }
 
     @Override
@@ -74,7 +83,9 @@
                     signature, SHORT_NAME);
             return;
         }
-        mConsumer.whitelistEntry(signature);
+
+        mAnnotationConsumer.consume(signature, stringifyAnnotationProperties(annotation),
+                ImmutableSet.of(mHiddenapiFlag));
     }
 
     private String findReturnType(AnnotationEntry a) {
diff --git a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java
index bd0bf79..f2bc525 100644
--- a/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/CovariantReturnTypeMultiHandler.java
@@ -18,7 +18,7 @@
  *
  * <p>The enclosed annotations are passed to {@link CovariantReturnTypeHandler}.
  */
-public class CovariantReturnTypeMultiHandler implements AnnotationHandler {
+public class CovariantReturnTypeMultiHandler extends AnnotationHandler {
 
     public static final String ANNOTATION_NAME =
             "Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;";
@@ -28,14 +28,15 @@
     private final CovariantReturnTypeHandler mWrappedHandler;
     private final String mInnerAnnotationName;
 
-    public CovariantReturnTypeMultiHandler(GreylistConsumer consumer, Set<String> publicApis) {
-        this(consumer, publicApis, CovariantReturnTypeHandler.ANNOTATION_NAME);
+    public CovariantReturnTypeMultiHandler(AnnotationConsumer consumer, Set<String> publicApis,
+            String hiddenapiFlag) {
+        this(consumer, publicApis, hiddenapiFlag, CovariantReturnTypeHandler.ANNOTATION_NAME);
     }
 
     @VisibleForTesting
-    public CovariantReturnTypeMultiHandler(GreylistConsumer consumer, Set<String> publicApis,
-            String innerAnnotationName) {
-        mWrappedHandler = new CovariantReturnTypeHandler(consumer, publicApis);
+    public CovariantReturnTypeMultiHandler(AnnotationConsumer consumer, Set<String> publicApis,
+            String hiddenapiFlag, String innerAnnotationName) {
+        mWrappedHandler = new CovariantReturnTypeHandler(consumer, publicApis, hiddenapiFlag);
         mInnerAnnotationName = innerAnnotationName;
     }
 
diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java
deleted file mode 100644
index 7d28b31..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/CsvGreylistConsumer.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.android.class2greylist;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.PrintStream;
-import java.util.Map;
-
-public class CsvGreylistConsumer implements GreylistConsumer {
-
-    private final Status mStatus;
-    private final CsvWriter mCsvWriter;
-
-    public CsvGreylistConsumer(Status status, String csvMetadataFile) throws FileNotFoundException {
-        mStatus = status;
-        mCsvWriter = new CsvWriter(
-                new PrintStream(new FileOutputStream(new File(csvMetadataFile))));
-    }
-
-    @Override
-    public void greylistEntry(String signature, Integer maxTargetSdk,
-            Map<String, String> annotationProperties) {
-        annotationProperties.put("signature", signature);
-        mCsvWriter.addRow(annotationProperties);
-    }
-
-    @Override
-    public void whitelistEntry(String signature) {
-    }
-
-    @Override
-    public void close() {
-        mCsvWriter.close();
-    }
-}
diff --git a/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java b/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java
deleted file mode 100644
index 3cfec30..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/CsvWriter.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.android.class2greylist;
-
-import com.google.common.base.Joiner;
-
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Helper class for writing data to a CSV file.
- *
- * This class does not write anything to its output until it is closed, so it can gather a set of
- * all columns before writing the header row.
- */
-public class CsvWriter {
-
-    private final PrintStream mOutput;
-    private final ArrayList<Map<String, String>> mContents;
-    private final Set<String> mColumns;
-
-    public CsvWriter(PrintStream out) {
-        mOutput = out;
-        mContents = new ArrayList<>();
-        mColumns = new HashSet<>();
-    }
-
-    public void addRow(Map<String, String> values) {
-        mColumns.addAll(values.keySet());
-        mContents.add(values);
-    }
-
-    public void close() {
-        List<String> columns = new ArrayList<>(mColumns);
-        columns.sort(Comparator.naturalOrder());
-        mOutput.println(columns.stream().collect(Collectors.joining(",")));
-        for (Map<String, String> row : mContents) {
-            mOutput.println(columns.stream().map(column -> row.getOrDefault(column, "")).collect(
-                    Collectors.joining(",")));
-        }
-        mOutput.close();
-    }
-
-
-}
diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
deleted file mode 100644
index b3ed1b1..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.android.class2greylist;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.PrintStream;
-import java.util.HashMap;
-import java.util.Map;
-
-public class FileWritingGreylistConsumer implements GreylistConsumer {
-
-    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)));
-    }
-
-    @VisibleForTesting
-    public static Map<Integer, PrintStream> openFiles(
-            Map<Integer, String> filenames) throws FileNotFoundException {
-        Map<String, PrintStream> streamsByName = new HashMap<>();
-        Map<Integer, PrintStream> streams = new HashMap<>();
-        for (Map.Entry<Integer, String> entry : filenames.entrySet()) {
-            if (!streamsByName.containsKey(entry.getValue())) {
-                streamsByName.put(entry.getValue(), openFile(entry.getValue()));
-            }
-            streams.put(entry.getKey(), streamsByName.get(entry.getValue()));
-        }
-        return streams;
-    }
-
-    public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap,
-            String whitelistFile) throws FileNotFoundException {
-        mStatus = status;
-        mSdkToPrintStreamMap = openFiles(sdkToFilenameMap);
-        mWhitelistStream = openFile(whitelistFile);
-    }
-
-    @Override
-    public void greylistEntry(
-            String signature, Integer maxTargetSdk, Map<String, String> annotationProperties) {
-        PrintStream p = mSdkToPrintStreamMap.get(maxTargetSdk);
-        if (p == null) {
-            mStatus.error("No output file for signature %s with maxTargetSdk of %d", signature,
-                    maxTargetSdk == null ? "<absent>" : maxTargetSdk.toString());
-            return;
-        }
-        p.println(signature);
-    }
-
-    @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
deleted file mode 100644
index 72c0ea4..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
+++ /dev/null
@@ -1,149 +0,0 @@
-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.HashMap;
-import java.util.Map;
-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 {
-
-    // 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 Predicate<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,
-            Predicate<Integer> validMaxTargetSdkValues) {
-        this(status, greylistConsumer,
-                member -> !(member.bridge && publicApis.contains(member.signature)),
-                validMaxTargetSdkValues);
-    }
-
-    @VisibleForTesting
-    public GreylistAnnotationHandler(
-            Status status,
-            GreylistConsumer greylistConsumer,
-            Predicate<GreylistMember> greylistFilter,
-            Predicate<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;
-        Map<String, String> allValues = new HashMap<String, String>();
-        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;
-            }
-            allValues.put(property.getNameString(), property.getValue().stringifyValue());
-        }
-        if (mGreylistFilter.test(new GreylistMember(signature, bridge, maxTargetSdk))) {
-            mGreylistConsumer.greylistEntry(signature, maxTargetSdk, allValues);
-        }
-    }
-
-    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());
-            return null;
-        }
-        int value = ((SimpleElementValue) property.getValue()).getValueInt();
-        if (!mValidMaxTargetSdkValues.test(value)) {
-            context.reportError("Invalid value for %s: got %d, expected one of [%s]",
-                    property.getNameString(),
-                    value,
-                    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
deleted file mode 100644
index afded37..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.android.class2greylist;
-
-import java.util.Map;
-
-public interface GreylistConsumer {
-    /**
-     * Handle a new greylist entry.
-     *
-     * @param signature Signature of the member.
-     * @param maxTargetSdk maxTargetSdk value from the annotation, or null if none set.
-     */
-    void greylistEntry(
-            String signature, Integer maxTargetSdk, Map<String, String> annotationProperties);
-
-    /**
-     * 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/HiddenapiFlagsWriter.java b/tools/class2greylist/src/com/android/class2greylist/HiddenapiFlagsWriter.java
new file mode 100644
index 0000000..54ca17c
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/HiddenapiFlagsWriter.java
@@ -0,0 +1,40 @@
+package com.android.class2greylist;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HiddenapiFlagsWriter implements AnnotationConsumer {
+
+    private final PrintStream mOutput;
+
+    public HiddenapiFlagsWriter(String csvFile) throws FileNotFoundException {
+        mOutput = new PrintStream(new FileOutputStream(new File(csvFile)));
+    }
+
+    public void consume(String apiSignature, Map<String, String> annotationProperties,
+            Set<String> parsedFlags) {
+        if (parsedFlags.size() > 0) {
+            mOutput.println(apiSignature + "," + String.join(",", asSortedList(parsedFlags)));
+        }
+    }
+
+    public void close() {
+        mOutput.close();
+    }
+
+    private static List<String> asSortedList(Set<String> s) {
+        List<String> list = new ArrayList<>(s);
+        Collections.sort(list);
+        return list;
+    }
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
deleted file mode 100644
index f86ac6e..0000000
--- a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.android.class2greylist;
-
-import java.util.Map;
-
-public class SystemOutGreylistConsumer implements GreylistConsumer {
-    @Override
-    public void greylistEntry(
-            String signature, Integer maxTargetSdk, Map<String, String> annotationValues) {
-        System.out.println(signature);
-    }
-
-    @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/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
new file mode 100644
index 0000000..d1f3e77
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
@@ -0,0 +1,134 @@
+package com.android.class2greylist;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+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.Map;
+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 UnsupportedAppUsageAnnotationHandler extends AnnotationHandler {
+
+    // properties of greylist annotations:
+    private static final String EXPECTED_SIGNATURE_PROPERTY = "expectedSignature";
+    private static final String MAX_TARGET_SDK_PROPERTY = "maxTargetSdk";
+
+    private final Status mStatus;
+    private final Predicate<ClassMember> mClassMemberFilter;
+    private final Map<Integer, String> mSdkVersionToFlagMap;
+    private final AnnotationConsumer mAnnotationConsumer;
+
+    /**
+     * Represents a member of a class file (a field or method).
+     */
+    @VisibleForTesting
+    public static class ClassMember {
+
+        /**
+         * Signature of this class member.
+         */
+        public final String signature;
+
+        /**
+         * Indicates if this is a synthetic bridge method.
+         */
+        public final boolean isBridgeMethod;
+
+        public ClassMember(String signature, boolean isBridgeMethod) {
+            this.signature = signature;
+            this.isBridgeMethod = isBridgeMethod;
+        }
+    }
+
+    public UnsupportedAppUsageAnnotationHandler(Status status,
+            AnnotationConsumer annotationConsumer, Set<String> publicApis,
+            Map<Integer, String> sdkVersionToFlagMap) {
+        this(status, annotationConsumer,
+                member -> !(member.isBridgeMethod && publicApis.contains(member.signature)),
+                sdkVersionToFlagMap);
+    }
+
+    @VisibleForTesting
+    public UnsupportedAppUsageAnnotationHandler(Status status,
+            AnnotationConsumer annotationConsumer, Predicate<ClassMember> memberFilter,
+            Map<Integer, String> sdkVersionToFlagMap) {
+        mStatus = status;
+        mAnnotationConsumer = annotationConsumer;
+        mClassMemberFilter = memberFilter;
+        mSdkVersionToFlagMap = sdkVersionToFlagMap;
+    }
+
+    @Override
+    public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) {
+        FieldOrMethod member = context.member;
+
+        boolean isBridgeMethod = (member instanceof Method) &&
+                (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
+        if (isBridgeMethod) {
+            mStatus.debug("Member is a bridge method");
+        }
+
+        String signature = context.getMemberDescriptor();
+        Integer maxTargetSdk = null;
+
+        for (ElementValuePair property : annotation.getElementValuePairs()) {
+            switch (property.getNameString()) {
+                case EXPECTED_SIGNATURE_PROPERTY:
+                    String expected = property.getValue().stringifyValue();
+                    // Don't enforce for bridge methods; they're generated so won't match.
+                    if (!isBridgeMethod && !signature.equals(expected)) {
+                        context.reportError("Expected signature does not match generated:\n"
+                                        + "Expected:  %s\n"
+                                        + "Generated: %s", expected, signature);
+                        return;
+                    }
+                    break;
+                case MAX_TARGET_SDK_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());
+                        return;
+                    }
+
+                    maxTargetSdk = ((SimpleElementValue) property.getValue()).getValueInt();
+                    break;
+            }
+        }
+
+        // Verify that maxTargetSdk is valid.
+        if (!mSdkVersionToFlagMap.containsKey(maxTargetSdk)) {
+            context.reportError("Invalid value for %s: got %d, expected one of [%s]",
+                    MAX_TARGET_SDK_PROPERTY,
+                    maxTargetSdk,
+                    mSdkVersionToFlagMap.keySet());
+            return;
+        }
+
+        // Consume this annotation if it matches the predicate.
+        if (mClassMemberFilter.test(new ClassMember(signature, isBridgeMethod))) {
+            mAnnotationConsumer.consume(signature, stringifyAnnotationProperties(annotation),
+                    ImmutableSet.of(mSdkVersionToFlagMap.get(maxTargetSdk)));
+        }
+    }
+}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
index 8f4a76f..65ebbf0 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
@@ -36,14 +36,14 @@
     public TestName mTestName = new TestName();
 
     protected Javac mJavac;
-    protected GreylistConsumer mConsumer;
+    protected AnnotationConsumer 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);
+        mConsumer = mock(AnnotationConsumer.class);
         mStatus = mock(Status.class, withSettings().verboseLogging());
         mJavac = new Javac();
     }
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
deleted file mode 100644
index b87a5b1..0000000
--- a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * 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;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.mockito.Mock;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class Class2GreylistTest {
-
-    @Mock
-    Status mStatus;
-    @Rule
-    public TestName mTestName = new TestName();
-
-    @Before
-    public void setup() throws IOException {
-        System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
-                mTestName.getMethodName()));
-        initMocks(this);
-    }
-
-    @Test
-    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 testReadGreylistMapNone() throws IOException {
-        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
-                new String[]{"none:noApi"});
-        verifyZeroInteractions(mStatus);
-        assertThat(map).containsExactly(null, "noApi");
-    }
-
-    @Test
-    public void testReadGreylistMapMulti() throws IOException {
-        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
-                new String[]{"1,none:noOr1Api", "3:apiThree"});
-        verifyZeroInteractions(mStatus);
-        assertThat(map).containsExactly(null, "noOr1Api", 1, "noOr1Api", 3, "apiThree");
-    }
-
-    @Test
-    public void testReadGreylistMapMulti2() throws IOException {
-        Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
-                new String[]{"1,none,2,3,4:allApi"});
-        verifyZeroInteractions(mStatus);
-        assertThat(map).containsExactly(
-                null, "allApi", 1, "allApi", 2, "allApi", 3, "allApi", 4, "allApi");
-    }
-
-    @Test
-    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.readGreylistMap(mStatus,
-                new String[]{"noApi", "anotherNoApi", "1:apiOne"});
-        verify(mStatus, atLeastOnce()).error(any(), any());
-    }
-
-    @Test
-    public void testReadGreylistMapInvalidInt() throws IOException {
-        Class2Greylist.readGreylistMap(mStatus, new String[]{"noApi", "a:apiOne"});
-        verify(mStatus, atLeastOnce()).error(any(), any());
-    }
-
-    @Test
-    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/CovariantReturnTypeHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java
index 10fae9b..9d2f014 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeHandlerTest.java
@@ -40,6 +40,7 @@
 public class CovariantReturnTypeHandlerTest extends AnnotationHandlerTestBase {
 
     private static final String ANNOTATION = "Lannotation/Annotation;";
+    private static final String FLAG = "test-flag";
 
     @Before
     public void setup() throws IOException {
@@ -72,11 +73,13 @@
                 ImmutableMap.of(ANNOTATION,
                         new CovariantReturnTypeHandler(
                                 mConsumer,
-                                ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;")));
+                                ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;"),
+                                FLAG));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
-        verify(mConsumer, times(1)).whitelistEntry(eq("La/b/Class;->method()Ljava/lang/Integer;"));
+        verify(mConsumer, times(1)).consume(
+                eq("La/b/Class;->method()Ljava/lang/Integer;"), any(), eq(ImmutableSet.of(FLAG)));
     }
 
     @Test
@@ -94,7 +97,8 @@
                 ImmutableMap.of(ANNOTATION,
                         new CovariantReturnTypeHandler(
                                 mConsumer,
-                                emptySet()));
+                                emptySet(),
+                                FLAG));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         verify(mStatus, atLeastOnce()).error(any(), any());
@@ -118,7 +122,8 @@
                                 ImmutableSet.of(
                                         "La/b/Class;->method()Ljava/lang/String;",
                                         "La/b/Class;->method()Ljava/lang/Integer;"
-                                )));
+                                ),
+                                FLAG));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         verify(mStatus, atLeastOnce()).error(any(), any());
@@ -139,7 +144,8 @@
                 ImmutableMap.of(ANNOTATION,
                         new CovariantReturnTypeHandler(
                                 mConsumer,
-                                emptySet()));
+                                emptySet(),
+                                FLAG));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         verify(mStatus, atLeastOnce()).error(any(), any());
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java
index 7f4ce62..1202564 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/CovariantReturnTypeMultiHandlerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -38,6 +39,7 @@
 
 public class CovariantReturnTypeMultiHandlerTest extends AnnotationHandlerTestBase {
 
+    private static final String FLAG = "test-flag";
 
     @Before
     public void setup() throws IOException {
@@ -79,16 +81,17 @@
                         new CovariantReturnTypeMultiHandler(
                                 mConsumer,
                                 ImmutableSet.of("La/b/Class;->method()Ljava/lang/String;"),
+                                FLAG,
                                 "Lannotation/Annotation;"));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> whitelist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(2)).whitelistEntry(whitelist.capture());
+        verify(mConsumer, times(2)).consume(whitelist.capture(), any(),
+                eq(ImmutableSet.of(FLAG)));
         assertThat(whitelist.getAllValues()).containsExactly(
                 "La/b/Class;->method()Ljava/lang/Integer;",
-                "La/b/Class;->method()Ljava/lang/Long;"
-        );
+                "La/b/Class;->method()Ljava/lang/Long;");
     }
 
     @Test
@@ -108,6 +111,7 @@
                         new CovariantReturnTypeMultiHandler(
                                 mConsumer,
                                 emptySet(),
+                                FLAG,
                                 "Lannotation/Annotation;"));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
 
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java
deleted file mode 100644
index 1e1b1df..0000000
--- a/tools/class2greylist/test/src/com/android/class2greylist/FileWritingGreylistConsumerTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.mockito.Mock;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-public class FileWritingGreylistConsumerTest {
-
-    @Mock
-    Status mStatus;
-    @Rule
-    public TestName mTestName = new TestName();
-    private int mFileNameSeq = 0;
-    private final List<String> mTempFiles = new ArrayList<>();
-
-    @Before
-    public void setup() throws IOException {
-        System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
-                mTestName.getMethodName()));
-        initMocks(this);
-    }
-
-    @After
-    public void removeTempFiles() {
-        for (String name : mTempFiles) {
-            new File(name).delete();
-        }
-    }
-
-    private String tempFileName() {
-        String name = String.format(Locale.US, "/tmp/test-%s-%d",
-                mTestName.getMethodName(), mFileNameSeq++);
-        mTempFiles.add(name);
-        return name;
-    }
-
-    @Test
-    public void testSimpleMap() throws FileNotFoundException {
-        Map<Integer, PrintStream> streams = FileWritingGreylistConsumer.openFiles(
-                ImmutableMap.of(1, tempFileName(), 2, tempFileName()));
-        assertThat(streams.keySet()).containsExactly(1, 2);
-        assertThat(streams.get(1)).isNotNull();
-        assertThat(streams.get(2)).isNotNull();
-        assertThat(streams.get(2)).isNotSameAs(streams.get(1));
-    }
-
-    @Test
-    public void testCommonMappings() throws FileNotFoundException {
-        String name = tempFileName();
-        Map<Integer, PrintStream> streams = FileWritingGreylistConsumer.openFiles(
-                ImmutableMap.of(1, name, 2, name));
-        assertThat(streams.keySet()).containsExactly(1, 2);
-        assertThat(streams.get(1)).isNotNull();
-        assertThat(streams.get(2)).isSameAs(streams.get(1));
-    }
-}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java b/tools/class2greylist/test/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandlerTest.java
similarity index 87%
rename from tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
rename to tools/class2greylist/test/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandlerTest.java
index edf2ecd..cdf01af 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandlerTest.java
@@ -19,11 +19,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import static java.util.Collections.emptySet;
+import static java.util.Collections.emptyMap;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
@@ -35,14 +36,23 @@
 import org.mockito.ArgumentCaptor;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Predicate;
 
-public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
+public class UnsupportedAppUsageAnnotationHandlerTest extends AnnotationHandlerTestBase {
 
     private static final String ANNOTATION = "Lannotation/Anno;";
 
+    private static final Map<Integer, String> NULL_SDK_MAP;
+    static {
+        Map<Integer, String> map = new HashMap<>();
+        map.put(null, "flag-null");
+        NULL_SDK_MAP = Collections.unmodifiableMap(map);
+    }
+
     @Before
     public void setup() throws IOException {
         mJavac.addSource("annotation.Anno", Joiner.on('\n').join(
@@ -56,11 +66,11 @@
                 "}"));
     }
 
-    private GreylistAnnotationHandler createGreylistHandler(
-            Predicate<GreylistAnnotationHandler.GreylistMember> greylistFilter,
-            Set<Integer> validMaxTargetSdkValues) {
-        return new GreylistAnnotationHandler(
-                mStatus, mConsumer, greylistFilter, x -> validMaxTargetSdkValues.contains(x));
+    private UnsupportedAppUsageAnnotationHandler createGreylistHandler(
+            Predicate<UnsupportedAppUsageAnnotationHandler.ClassMember> greylistFilter,
+            Map<Integer, String> validMaxTargetSdkValues) {
+        return new UnsupportedAppUsageAnnotationHandler(
+                mStatus, mConsumer, greylistFilter, validMaxTargetSdkValues);
     }
 
     @Test
@@ -75,12 +85,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
     }
 
@@ -96,12 +106,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V");
     }
 
@@ -117,12 +127,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I");
     }
 
@@ -138,12 +148,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
     }
 
@@ -159,7 +169,7 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         verify(mStatus, times(1)).error(any(), any());
@@ -179,12 +189,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V");
     }
 
@@ -198,11 +208,11 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
-        verify(mConsumer, never()).greylistEntry(any(String.class), any(), any());
+        verify(mConsumer, never()).consume(any(String.class), any(), any());
     }
 
     @Test
@@ -217,12 +227,12 @@
         assertThat(mJavac.compile()).isTrue();
 
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V");
     }
 
@@ -245,14 +255,14 @@
         assertThat(mJavac.compile()).isTrue();
 
         Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP));
         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);
         // A bridge method is generated for the above, so we expect 2 greylist entries.
-        verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(2)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getAllValues()).containsExactly(
                 "La/b/Class;->method(Ljava/lang/Object;)V",
                 "La/b/Class;->method(Ljava/lang/String;)V");
@@ -277,14 +287,14 @@
         assertThat(mJavac.compile()).isTrue();
 
         Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP));
         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);
         // A bridge method is generated for the above, so we expect 2 greylist entries.
-        verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(2)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getAllValues()).containsExactly(
                 "La/b/Class;->method(Ljava/lang/Object;)V",
                 "La/b/Class;->method(Ljava/lang/String;)V");
@@ -313,7 +323,7 @@
         assertThat(mJavac.compile()).isTrue();
 
         Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Interface"), mStatus, handlerMap)
                 .visit();
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
@@ -322,7 +332,7 @@
         assertNoErrors();
         ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
         // A bridge method is generated for the above, so we expect 2 greylist entries.
-        verify(mConsumer, times(2)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(2)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getAllValues()).containsExactly(
                 "La/b/Class;->method(Ljava/lang/Object;)V",
                 "La/b/Base;->method(Ljava/lang/Object;)V");
@@ -351,18 +361,18 @@
                 "La/b/Class;->method(Ljava/lang/Object;)V");
         Map<String, AnnotationHandler> handlerMap =
                 ImmutableMap.of(ANNOTATION,
-                        new GreylistAnnotationHandler(
+                        new UnsupportedAppUsageAnnotationHandler(
                                 mStatus,
                                 mConsumer,
                                 publicApis,
-                                x -> false));
+                                NULL_SDK_MAP));
         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);
         // The bridge method generated for the above, is a public API so should be excluded
-        verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method(Ljava/lang/String;)V");
     }
 
@@ -379,12 +389,12 @@
 
         Map<String, AnnotationHandler> handlerMap =
                 ImmutableMap.of(ANNOTATION, createGreylistHandler(
-                        member -> !member.bridge, // exclude bridge methods
-                        emptySet()));
+                        member -> !member.isBridgeMethod, // exclude bridge methods
+                        NULL_SDK_MAP));
         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(), any());
+        verify(mConsumer, times(1)).consume(greylist.capture(), any(), any());
         assertThat(greylist.getValue()).isEqualTo("La/b/Class;->field:I");
     }
 
@@ -400,7 +410,7 @@
         assertThat(mJavac.compile()).isTrue();
 
         Map<String, AnnotationHandler> handlerMap =
-                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+                ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, NULL_SDK_MAP));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         verify(mStatus, times(1)).error(any(), any());
     }
@@ -419,12 +429,10 @@
         Map<String, AnnotationHandler> handlerMap =
                 ImmutableMap.of(ANNOTATION, createGreylistHandler(
                         x -> true,
-                        ImmutableSet.of(1)));
+                        ImmutableMap.of(1, "flag1")));
         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(), any());
-        assertThat(maxTargetSdk.getValue()).isEqualTo(1);
+        verify(mConsumer, times(1)).consume(any(), any(), eq(ImmutableSet.of("flag1")));
     }
 
     @Test
@@ -441,12 +449,10 @@
         Map<String, AnnotationHandler> handlerMap =
                 ImmutableMap.of(ANNOTATION, createGreylistHandler(
                         x -> true,
-                        ImmutableSet.of(1)));
+                        NULL_SDK_MAP));
         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(), any());
-        assertThat(maxTargetSdk.getValue()).isEqualTo(null);
+        verify(mConsumer, times(1)).consume(any(), any(), eq(ImmutableSet.of("flag-null")));
     }
 
     @Test
@@ -463,7 +469,7 @@
         Map<String, AnnotationHandler> handlerMap =
                 ImmutableMap.of(ANNOTATION, createGreylistHandler(
                         x -> true,
-                        ImmutableSet.of(1)));
+                        NULL_SDK_MAP));
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
         verify(mStatus, times(1)).error(any(), any());
     }
@@ -490,12 +496,12 @@
         assertThat(mJavac.compile()).isTrue();
         new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
                 ImmutableMap.of("Lannotation/Anno2;", createGreylistHandler(x -> true,
-                        ImmutableSet.of(2)))
+                        ImmutableMap.of(2, "flag2")))
         ).visit();
 
         assertNoErrors();
         ArgumentCaptor<Map<String, String>> properties = ArgumentCaptor.forClass(Map.class);
-        verify(mConsumer, times(1)).greylistEntry(any(), any(), properties.capture());
+        verify(mConsumer, times(1)).consume(any(), properties.capture(), any());
         assertThat(properties.getValue()).containsExactly(
                 "maxTargetSdk", "2",
                 "trackingBug", "123456789");