Merge 1714fa197bd351843ab8469a79f8cecbe03d8c4c on remote branch

Change-Id: I956012c08eb2f08040b2d41634319282ba2da7a6
diff --git a/Android.mk b/Android.mk
index 7f6aa4b..5955b0f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,3 +17,10 @@
 ifneq ($INTERNAL_PLATFORM_MERGED_COMPAT_CONFIG,)
 $(call dist-for-goals,droidcore,$(INTERNAL_PLATFORM_MERGED_COMPAT_CONFIG))
 endif
+
+# ==== hiddenapi lists =======================================
+ifneq ($(UNSAFE_DISABLE_HIDDENAPI_FLAGS),true)
+$(call dist-for-goals,droidcore,$(INTERNAL_PLATFORM_HIDDENAPI_FLAGS))
+$(call dist-for-goals,droidcore,$(INTERNAL_PLATFORM_HIDDENAPI_GREYLIST_METADATA))
+$(call dist-for-goals,droidcore,$(INTERNAL_PLATFORM_HIDDENAPI_INDEX))
+endif  # UNSAFE_DISABLE_HIDDENAPI_FLAGS
diff --git a/java/android/compat/annotation/Android.bp b/java/android/compat/annotation/Android.bp
index fb88748..c607e5d 100644
--- a/java/android/compat/annotation/Android.bp
+++ b/java/android/compat/annotation/Android.bp
@@ -23,10 +23,11 @@
         "EnabledAfter.java",
         "LoggingOnly.java",
     ],
-    // Allow core_current to use the annotations.
-    sdk_version: "current",
+    sdk_version: "core_current",
+    exported_plugins: ["compat-changeid-annotation-processor"],
 }
 
+// Don't forget to depend on exported_plugins, if using sources directly
 filegroup {
     name: "app-compat-annotations-source",
     srcs: [
@@ -36,6 +37,7 @@
         "LoggingOnly.java",
         "UnsupportedAppUsage.java",
     ],
+    visibility: ["//libcore:__pkg__"],
 }
 
 java_library {
@@ -45,4 +47,5 @@
         "UnsupportedAppUsage.java",
     ],
     sdk_version: "core_current",
+    exported_plugins: ["unsupportedappusage-annotation-processor"],
 }
diff --git a/java/android/processor/compat/Android.bp b/java/android/processor/compat/Android.bp
new file mode 100644
index 0000000..09721f7
--- /dev/null
+++ b/java/android/processor/compat/Android.bp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+java_library_host {
+    name: "compat-processor",
+    srcs: [
+        "*.java",
+    ],
+    static_libs: [
+        "guava",
+    ],
+    openjdk9: {
+        javacflags: [
+            "--add-modules=jdk.compiler",
+            "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
+            "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
+            "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+            "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+        ],
+    },
+
+    visibility: ["//tools/platform-compat/java/android/processor/compat:__subpackages__"],
+}
diff --git a/java/android/processor/compat/SingleAnnotationProcessor.java b/java/android/processor/compat/SingleAnnotationProcessor.java
new file mode 100644
index 0000000..df5afe1
--- /dev/null
+++ b/java/android/processor/compat/SingleAnnotationProcessor.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2020 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 android.processor.compat;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Table;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.LineMap;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.SourcePositions;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.Trees;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Abstract annotation processor that goes over annotated elements in bulk (per .class file).
+ *
+ * <p>It expects only one supported annotation, i.e. {@link #getSupportedAnnotationTypes()} must
+ * return one annotation type only.
+ *
+ * <p>Annotated elements are pre-filtered by {@link #ignoreAnnotatedElement(Element,
+ * AnnotationMirror)}. Afterwards, the table with package and enclosing element name into list of
+ * elements is generated and passed to {@link #process(TypeElement, Table)}.
+ */
+public abstract class SingleAnnotationProcessor extends AbstractProcessor {
+
+    protected Elements elements;
+    protected Messager messager;
+    protected SourcePositions sourcePositions;
+    protected Trees trees;
+    protected Types types;
+
+    @Override
+    public synchronized void init(ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+
+        this.elements = processingEnv.getElementUtils();
+        this.messager = processingEnv.getMessager();
+        this.trees = Trees.instance(processingEnv);
+        this.types = processingEnv.getTypeUtils();
+
+        this.sourcePositions = trees.getSourcePositions();
+    }
+
+    @Override
+    public boolean process(
+            Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
+        if (annotations.size() == 0) {
+            // no annotations to process, doesn't really matter what we return here.
+            return true;
+        }
+
+        TypeElement annotation = Iterables.getOnlyElement(annotations);
+        String supportedAnnotation = Iterables.getOnlyElement(getSupportedAnnotationTypes());
+        Preconditions.checkState(supportedAnnotation.equals(annotation.toString()));
+
+        Table<PackageElement, String, List<Element>> annotatedElements = HashBasedTable.create();
+        for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(annotation)) {
+            AnnotationMirror annotationMirror =
+                    getSupportedAnnotationMirror(annotation, annotatedElement);
+            if (ignoreAnnotatedElement(annotatedElement, annotationMirror)) {
+                continue;
+            }
+
+            PackageElement packageElement = elements.getPackageOf(annotatedElement);
+            String enclosingElementName = getEnclosingElementName(annotatedElement);
+            Preconditions.checkNotNull(packageElement);
+            Preconditions.checkNotNull(enclosingElementName);
+
+            if (!annotatedElements.contains(packageElement, enclosingElementName)) {
+                annotatedElements.put(packageElement, enclosingElementName, new ArrayList<>());
+            }
+            annotatedElements.get(packageElement, enclosingElementName).add(annotatedElement);
+        }
+
+        process(annotation, annotatedElements);
+        return true;
+    }
+
+    /**
+     * Processes a set of elements annotated with supported annotation and not ignored via {@link
+     * #ignoreAnnotatedElement(Element, AnnotationMirror)}.
+     *
+     * @param annotation        {@link TypeElement} of the supported annotation
+     * @param annotatedElements table with {@code package}, {@code enclosing elements name}, and the
+     *                          list of elements
+     */
+    protected abstract void process(TypeElement annotation,
+            Table<PackageElement, String, List<Element>> annotatedElements);
+
+    /** Whether to process given element with the annotation mirror. */
+    protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
+        return false;
+    }
+
+    /**
+     * Returns the annotation mirror for the supported annotation on the given element.
+     *
+     * <p>We are not using a class to avoid choosing which Java 9 Module to select from, in case
+     * the annotation is present in base module and unnamed module.
+     */
+    protected final AnnotationMirror getSupportedAnnotationMirror(TypeElement annotation,
+            Element element) {
+        for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
+            if (types.isSameType(annotation.asType(), mirror.getAnnotationType())) {
+                return mirror;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns {@link SourcePosition} of an annotation on the given element or null if position is
+     * not found.
+     */
+    @Nullable
+    protected final SourcePosition getSourcePosition(Element element,
+            AnnotationMirror annotationMirror) {
+        TreePath path = trees.getPath(element, annotationMirror);
+        if (path == null) {
+            return null;
+        }
+        CompilationUnitTree compilationUnit = path.getCompilationUnit();
+        Tree tree = path.getLeaf();
+        long startPosition = sourcePositions.getStartPosition(compilationUnit, tree);
+        long endPosition = sourcePositions.getEndPosition(compilationUnit, tree);
+
+        LineMap lineMap = path.getCompilationUnit().getLineMap();
+        return new SourcePosition(
+                compilationUnit.getSourceFile().getName(),
+                lineMap.getLineNumber(startPosition),
+                lineMap.getColumnNumber(startPosition),
+                lineMap.getLineNumber(endPosition),
+                lineMap.getColumnNumber(endPosition));
+    }
+
+    @Nullable
+    protected final AnnotationValue getAnnotationValue(
+            Element element, AnnotationMirror annotation, String propertyName) {
+        return annotation.getElementValues().keySet().stream()
+                .filter(key -> propertyName.equals(key.getSimpleName().toString()))
+                .map(key -> annotation.getElementValues().get(key))
+                .reduce((a, b) -> {
+                    throw new IllegalStateException(
+                            String.format("Only one %s expected, found %s in %s",
+                                    propertyName, annotation, element));
+                })
+                .orElse(null);
+    }
+
+    /**
+     * Returns a name of an enclosing element without the package name.
+     *
+     * <p>This would return names of all enclosing classes, e.g. <code>Outer.Inner.Foo</code>.
+     */
+    private String getEnclosingElementName(Element element) {
+        String fullQualifiedName =
+                ((QualifiedNameable) element.getEnclosingElement()).getQualifiedName().toString();
+        String packageName = elements.getPackageOf(element).toString();
+        return fullQualifiedName.substring(packageName.length() + 1);
+    }
+
+}
diff --git a/java/android/processor/compat/SourcePosition.java b/java/android/processor/compat/SourcePosition.java
new file mode 100644
index 0000000..0a27899
--- /dev/null
+++ b/java/android/processor/compat/SourcePosition.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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 android.processor.compat;
+
+import java.util.Objects;
+
+/** POJO to represent source position of a tree in Java source file. */
+public final class SourcePosition {
+
+    private final String filename;
+    private final long startLineNumber;
+    private final long startColumnNumber;
+    private final long endLineNumber;
+    private final long endColumnNumber;
+
+    public SourcePosition(String filename, long startLineNumber, long startColumnNumber,
+            long endLineNumber, long endColumnNumber) {
+        this.filename = filename;
+        this.startLineNumber = startLineNumber;
+        this.startColumnNumber = startColumnNumber;
+        this.endLineNumber = endLineNumber;
+        this.endColumnNumber = endColumnNumber;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public long getStartLineNumber() {
+        return startLineNumber;
+    }
+
+    public long getStartColumnNumber() {
+        return startColumnNumber;
+    }
+
+    public long getEndLineNumber() {
+        return endLineNumber;
+    }
+
+    public long getEndColumnNumber() {
+        return endColumnNumber;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SourcePosition that = (SourcePosition) o;
+        return startLineNumber == that.startLineNumber
+                && startColumnNumber == that.startColumnNumber
+                && endLineNumber == that.endLineNumber
+                && endColumnNumber == that.endColumnNumber
+                && Objects.equals(filename, that.filename);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                filename,
+                startLineNumber,
+                startColumnNumber,
+                endLineNumber,
+                endColumnNumber);
+    }
+}
diff --git a/java/android/processor/compat/TEST_MAPPING b/java/android/processor/compat/TEST_MAPPING
new file mode 100644
index 0000000..adba4ca
--- /dev/null
+++ b/java/android/processor/compat/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "unsupportedappusage-processor-test",
+      "host" : true
+    },
+    {
+      "name": "compat-changeid-annotation-processor-test",
+      "host" : true
+    }
+  ]
+}
diff --git a/java/android/processor/compat/changeid/Android.bp b/java/android/processor/compat/changeid/Android.bp
index 2cfe509..3bd831a 100644
--- a/java/android/processor/compat/changeid/Android.bp
+++ b/java/android/processor/compat/changeid/Android.bp
@@ -25,6 +25,7 @@
 
     static_libs: [
         "guava",
+        "//tools/platform-compat/java/android/processor/compat:compat-processor",
     ],
 
     openjdk9: {
@@ -44,8 +45,6 @@
     name: "compat-changeid-annotation-processor",
     processor_class: "android.processor.compat.changeid.ChangeIdProcessor",
 
-    visibility: ["//visibility:public"],
-
     java_resources: [
         "META-INF/**/*",
     ],
@@ -56,4 +55,9 @@
     installable: false,
 
     use_tools_jar: true,
+
+    visibility: [
+        "//libcore:__pkg__",
+        "//tools/platform-compat/java/android/compat/annotation:__subpackages__",
+    ],
 }
diff --git a/java/android/processor/compat/changeid/Change.java b/java/android/processor/compat/changeid/Change.java
index a3ea7b9..3054a67 100644
--- a/java/android/processor/compat/changeid/Change.java
+++ b/java/android/processor/compat/changeid/Change.java
@@ -16,13 +16,10 @@
 
 package android.processor.compat.changeid;
 
-import com.google.common.annotations.VisibleForTesting;
-
 /**
  * Simple data class that represents a change, built from the code annotations.
  */
-@VisibleForTesting
-public class Change {
+final class Change {
     final Long id;
     final String name;
     final boolean disabled;
@@ -46,8 +43,7 @@
      */
     final String sourcePosition;
 
-    @VisibleForTesting
-    public Change(Long id, String name, boolean disabled, boolean loggingOnly, Integer enabledAfter,
+     Change(Long id, String name, boolean disabled, boolean loggingOnly, Integer enabledAfter,
             String description, String javaPackage, String className, String qualifiedClass,
             String sourcePosition) {
         this.id = id;
@@ -74,7 +70,7 @@
         String qualifiedClass;
         String sourcePosition;
 
-        public Builder() {
+        Builder() {
         }
 
         public Builder id(long id) {
@@ -117,7 +113,7 @@
             return this;
         }
 
-        public Builder qualifedClass(String className) {
+        public Builder qualifiedClass(String className) {
             this.qualifiedClass = className;
             return this;
         }
@@ -129,9 +125,7 @@
 
         public Change build() {
             return new Change(id, name, disabled, loggingOnly, enabledAfter, description,
-                    javaPackage, javaClass,
-                    qualifiedClass, sourcePosition);
+                    javaPackage, javaClass, qualifiedClass, sourcePosition);
         }
-
     }
 }
diff --git a/java/android/processor/compat/changeid/ChangeIdProcessor.java b/java/android/processor/compat/changeid/ChangeIdProcessor.java
index fd43183..6a7d1e8 100644
--- a/java/android/processor/compat/changeid/ChangeIdProcessor.java
+++ b/java/android/processor/compat/changeid/ChangeIdProcessor.java
@@ -16,43 +16,36 @@
 
 package android.processor.compat.changeid;
 
+import static javax.lang.model.element.ElementKind.CLASS;
+import static javax.lang.model.element.ElementKind.PARAMETER;
 import static javax.tools.Diagnostic.Kind.ERROR;
 import static javax.tools.StandardLocation.CLASS_OUTPUT;
 
-import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import android.processor.compat.SingleAnnotationProcessor;
+import android.processor.compat.SourcePosition;
+
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
-import com.sun.tools.javac.model.JavacElements;
-import com.sun.tools.javac.tree.JCTree;
-import com.sun.tools.javac.util.Pair;
-import com.sun.tools.javac.util.Position;
+import com.google.common.collect.Table;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.Objects;
 import java.util.regex.Pattern;
 
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Messager;
-import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.AnnotationMirror;
 import javax.lang.model.element.AnnotationValue;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
-import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.Modifier;
 import javax.lang.model.element.PackageElement;
-import javax.lang.model.element.QualifiedNameable;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.TypeKind;
+import javax.tools.FileObject;
 
 /**
  * Annotation processor for ChangeId annotations.
@@ -62,245 +55,181 @@
  * Design doc: go/gating-and-logging.
  */
 @SupportedAnnotationTypes({"android.compat.annotation.ChangeId"})
-public class ChangeIdProcessor extends AbstractProcessor {
+@SupportedSourceVersion(SourceVersion.RELEASE_9)
+public class ChangeIdProcessor extends SingleAnnotationProcessor {
 
     private static final String CONFIG_XML = "compat_config.xml";
 
+    private static final String IGNORED_CLASS = "android.compat.Compatibility";
     private static final ImmutableSet<String> IGNORED_METHOD_NAMES =
             ImmutableSet.of("reportChange", "isChangeEnabled");
-    private static final String IGNORED_CLASS = "android.compat.Compatibility";
 
-    private static final String SUPPORTED_ANNOTATION =
+    private static final String CHANGE_ID_QUALIFIED_CLASS_NAME =
             "android.compat.annotation.ChangeId";
 
     private static final String DISABLED_CLASS_NAME = "android.compat.annotation.Disabled";
-    private static final String LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly";
     private static final String ENABLED_AFTER_CLASS_NAME = "android.compat.annotation.EnabledAfter";
+    private static final String LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly";
     private static final String TARGET_SDK_VERSION = "targetSdkVersion";
 
     private static final Pattern JAVADOC_SANITIZER = Pattern.compile("^\\s", Pattern.MULTILINE);
     private static final Pattern HIDE_TAG_MATCHER = Pattern.compile("(\\s|^)@hide(\\s|$)");
 
-    /**
-     * Used as a map key when sharding by classname.
-     */
-    class PackageClass {
-        final String javaPackage;
-        final String javaClass;
+    @Override
+    protected void process(TypeElement annotation,
+            Table<PackageElement, String, List<Element>> annotatedElements) {
+        for (PackageElement packageElement : annotatedElements.rowKeySet()) {
+            for (String enclosingElementName : annotatedElements.row(packageElement).keySet()) {
+                XmlWriter writer = new XmlWriter();
+                for (Element element : annotatedElements.get(packageElement,
+                        enclosingElementName)) {
+                    Change change =
+                            createChange(packageElement.toString(), enclosingElementName, element);
+                    writer.addChange(change);
+                }
 
-        PackageClass(String pkg, String cls) {
-            this.javaPackage = pkg;
-            this.javaClass = cls;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof PackageClass) {
-                PackageClass that = (PackageClass) obj;
-                return Objects.equal(this.javaPackage, that.javaPackage) &&
-                        Objects.equal(this.javaClass, that.javaClass);
+                try {
+                    FileObject resource = processingEnv.getFiler().createResource(
+                            CLASS_OUTPUT, packageElement.toString(),
+                            enclosingElementName + "_" + CONFIG_XML);
+                    try (OutputStream outputStream = resource.openOutputStream()) {
+                        writer.write(outputStream);
+                    }
+                } catch (IOException e) {
+                    messager.printMessage(ERROR, "Failed to write output: " + e);
+                }
             }
-            return false;
-        }
-
-        public int hashCode() {
-            return Objects.hashCode(javaPackage, javaClass);
         }
     }
 
     @Override
-    public SourceVersion getSupportedSourceVersion() {
-        return SourceVersion.latest();
-    }
-
-    /**
-     * This is the main entry point in the processor, called by the compiler.
-     */
-    @Override
-    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
-        Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(
-                processingEnv.getElementUtils().getTypeElement(
-                        SUPPORTED_ANNOTATION));
-        if (annotatedElements.isEmpty()) {
-            return true;
-        }
-
-        Map<PackageClass, XmlWriter> writersByClass = new HashMap<>();
-
-        for (Element e : annotatedElements) {
-            if (!isValidChangeId(e, processingEnv.getMessager())) {
-                continue;
-            }
-            Change change = createChange(e, processingEnv.getMessager(),
-                    processingEnv.getElementUtils().getDocComment(e));
-            PackageClass key = new PackageClass(change.javaPackage, change.className);
-            XmlWriter writer = writersByClass.get(key);
-            if (writer == null) {
-                writer = new XmlWriter();
-                writersByClass.put(key, writer);
-            }
-            writer.addChange(change);
-        }
-
-        for (Map.Entry<PackageClass, XmlWriter> entry : writersByClass.entrySet()) {
-            PackageClass key = entry.getKey();
-            try (OutputStream output = processingEnv.getFiler().createResource(
-                    CLASS_OUTPUT,
-                    key.javaPackage,
-                    key.javaClass + "_" + CONFIG_XML)
-                    .openOutputStream()) {
-                entry.getValue().write(output);
-            } catch (IOException e) {
-                throw new RuntimeException("Failed to write output for " + entry.getKey(), e);
-            }
-        }
-
-
-        return true;
-    }
-
-
-    private boolean shouldIgnoreAnnotation(Element e) {
-        // Just ignore the annotations on function known methods in package android.compat
+    protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
+        // Ignore the annotations on method parameters in known methods in package android.compat
         // (libcore/luni/src/main/java/android/compat/Compatibility.java)
         // without generating an error.
-        return (e.getKind() == ElementKind.PARAMETER
-                && e.getEnclosingElement().getKind() == ElementKind.METHOD
-                && IGNORED_METHOD_NAMES.contains(e.getEnclosingElement().getSimpleName().toString())
-                && e.getEnclosingElement().getEnclosingElement().getKind() == ElementKind.CLASS
-                && ((TypeElement) e.getEnclosingElement().getEnclosingElement()).getQualifiedName()
-                .toString().equals(IGNORED_CLASS));
+        if (element.getKind() == PARAMETER) {
+            Element enclosingMethod = element.getEnclosingElement();
+            Element enclosingElement = enclosingMethod.getEnclosingElement();
+            if (enclosingElement.getKind() == CLASS) {
+                if (enclosingElement.toString().equals(IGNORED_CLASS) &&
+                        IGNORED_METHOD_NAMES.contains(enclosingMethod.getSimpleName().toString())) {
+                    return true;
+                }
+            }
+        }
+        return !isValidChangeId(element);
     }
 
     /**
      * Checks if the provided java element is a valid change id (i.e. a long parameter with a
      * constant value).
      *
-     * @param e        java element to check.
-     * @param messager updated with compilation errors if the annotated element is not valid.
+     * @param element java element to check.
      * @return true if the provided element is a legal change id that should be added to the
-     * produced XML file. If true is returned it's guaranteed that the following
-     * operations are safe.
+     * produced XML file. If true is returned it's guaranteed that the following operations are
+     * safe.
      */
-    private boolean isValidChangeId(Element e, Messager messager) {
-        if (shouldIgnoreAnnotation(e)) {
-            return false;
-        }
-        if (e.getKind() != ElementKind.FIELD) {
+    private boolean isValidChangeId(Element element) {
+        if (element.getKind() != ElementKind.FIELD) {
             messager.printMessage(
                     ERROR,
-                    String.format(
-                            "Non field element %s annotated with @ChangeId. Got type "
-                                    + "%s, expected FIELD.",
-                            e.getSimpleName().toString(), e.getKind().toString()),
-                    e);
+                    "Non FIELD element annotated with @ChangeId.",
+                    element);
             return false;
         }
-        if (!(e instanceof VariableElement)) {
+        if (!(element instanceof VariableElement)) {
             messager.printMessage(
                     ERROR,
-                    String.format(
-                            "Non variable %s annotated with @ChangeId.",
-                            e.getSimpleName().toString()),
-                    e);
+                    "Non variable annotated with @ChangeId.",
+                    element);
             return false;
         }
-        if (((VariableElement) e).getConstantValue() == null) {
+        if (((VariableElement) element).getConstantValue() == null) {
             messager.printMessage(
                     ERROR,
-                    String.format(
-                            "Non constant/final variable %s (or non constant value) "
-                                    + "annotated with @ChangeId.",
-                            e.getSimpleName().toString()),
-                    e);
+                    "Non constant/final variable annotated with @ChangeId.",
+                    element);
             return false;
         }
-        if (e.asType().getKind() != TypeKind.LONG) {
+        if (element.asType().getKind() != TypeKind.LONG) {
             messager.printMessage(
                     ERROR,
-                    "Variables annotated with @ChangeId should be of type long.",
-                    e);
+                    "Variables annotated with @ChangeId must be of type long.",
+                    element);
             return false;
         }
-        if (!e.getModifiers().contains(Modifier.STATIC)) {
+        if (!element.getModifiers().contains(Modifier.STATIC)) {
             messager.printMessage(
                     ERROR,
-                    String.format(
-                            "Non static variable %s annotated with @ChangeId.",
-                            e.getSimpleName().toString()),
-                    e);
+                    "Non static variable annotated with @ChangeId.",
+                    element);
             return false;
         }
         return true;
     }
 
-    private String getSourcePosition(Element e, AnnotationMirror a) {
-        JavacElements javacElem = (JavacElements) processingEnv.getElementUtils();
-        Pair<JCTree, JCTree.JCCompilationUnit> pair = javacElem.getTreeAndTopLevel(e, a, null);
-        Position.LineMap lines = pair.snd.lineMap;
-        return String.format("%s:%d", pair.snd.getSourceFile().getName(),
-                lines.getLineNumber(pair.fst.pos().getStartPosition()));
-    }
-
-    private Change createChange(Element e, Messager messager, String comment) {
+    private Change createChange(String packageName, String enclosingElementName, Element element) {
         Change.Builder builder = new Change.Builder()
-                .id((Long) ((VariableElement) e).getConstantValue())
-                .name(e.getSimpleName().toString());
+                .id((Long) ((VariableElement) element).getConstantValue())
+                .name(element.getSimpleName().toString());
 
         AnnotationMirror changeId = null;
-        for (AnnotationMirror m : e.getAnnotationMirrors()) {
+        for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
             String type =
-                    ((TypeElement) m.getAnnotationType().asElement()).getQualifiedName().toString();
-            if (type.equals(DISABLED_CLASS_NAME)) {
-                builder.disabled();
-            } else if (type.equals(LOGGING_CLASS_NAME)) {
-                builder.loggingOnly();
-            } else if (type.equals(ENABLED_AFTER_CLASS_NAME)) {
-                for (Map.Entry<?, ?> entry : m.getElementValues().entrySet()) {
-                    String key = ((ExecutableElement) entry.getKey()).getSimpleName().toString();
-                    if (key.equals(TARGET_SDK_VERSION)) {
-                        builder.enabledAfter(
-                                (Integer) ((AnnotationValue) entry.getValue()).getValue());
-                    }
-                }
-            } else if (type.equals(SUPPORTED_ANNOTATION)) {
-                changeId = m;
+                    ((TypeElement) mirror.getAnnotationType().asElement()).getQualifiedName().toString();
+            switch (type) {
+                case DISABLED_CLASS_NAME:
+                    builder.disabled();
+                    break;
+                case LOGGING_CLASS_NAME:
+                    builder.loggingOnly();
+                    break;
+                case ENABLED_AFTER_CLASS_NAME:
+                    AnnotationValue value = getAnnotationValue(element, mirror, TARGET_SDK_VERSION);
+                    builder.enabledAfter((Integer)(Objects.requireNonNull(value).getValue()));
+                    break;
+                case CHANGE_ID_QUALIFIED_CLASS_NAME:
+                    changeId = mirror;
+                    break;
             }
         }
 
+        String comment =
+                elements.getDocComment(element);
         if (comment != null) {
             comment = HIDE_TAG_MATCHER.matcher(comment).replaceAll("");
             comment = JAVADOC_SANITIZER.matcher(comment).replaceAll("");
-            builder.description(comment.replaceAll("\\n"," ").trim());
+            comment = comment.replaceAll("\\n", " ");
+            builder.description(comment.trim());
         }
 
-        // TODO(satayev): move common processors code to android.processor.compat.
-        String packageName = processingEnv.getElementUtils().getPackageOf(e).toString();
-        String enclosingElementName = ((QualifiedNameable) e.getEnclosingElement()).getQualifiedName().toString();
-        String className = enclosingElementName.substring(packageName.length() + 1);
+        return verifyChange(element,
+                builder.javaClass(enclosingElementName)
+                        .javaPackage(packageName)
+                        .qualifiedClass(packageName + "." + enclosingElementName)
+                        .sourcePosition(getLineNumber(element, changeId))
+                        .build());
+    }
 
-        Change change = builder.javaClass(className)
-                .javaPackage(packageName)
-                .qualifedClass(enclosingElementName)
-                .sourcePosition(getSourcePosition(e, changeId))
-                .build();
+    private String getLineNumber(Element element, AnnotationMirror mirror) {
+        SourcePosition position = Objects.requireNonNull(getSourcePosition(element, mirror));
+        return String.format("%s:%d", position.getFilename(), position.getStartLineNumber());
+    }
 
+    private Change verifyChange(Element element, Change change) {
         if (change.disabled && change.enabledAfter != null) {
             messager.printMessage(
                     ERROR,
                     "ChangeId cannot be annotated with both @Disabled and @EnabledAfter.",
-                    e);
+                    element);
         }
-
         if (change.loggingOnly && (change.disabled || change.enabledAfter != null)) {
             messager.printMessage(
                     ERROR,
-                    "ChangeId cannot be annotated with both @LoggingOnly and @EnabledAfter or "
-                            + "@Disabled.",
-                    e);
+                    "ChangeId cannot be annotated with both @LoggingOnly and "
+                            + "(@EnabledAfter | @Disabled).",
+                    element);
         }
-
         return change;
     }
-
 }
diff --git a/java/android/processor/compat/changeid/TEST_MAPPING b/java/android/processor/compat/changeid/TEST_MAPPING
deleted file mode 100644
index 06a8fe6..0000000
--- a/java/android/processor/compat/changeid/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "compat-changeid-annotation-processor-test",
-      "host" : true
-    }
-  ]
-}
diff --git a/java/android/processor/compat/changeid/XmlWriter.java b/java/android/processor/compat/changeid/XmlWriter.java
index 484c0ba..eabacdb 100644
--- a/java/android/processor/compat/changeid/XmlWriter.java
+++ b/java/android/processor/compat/changeid/XmlWriter.java
@@ -16,11 +16,10 @@
 
 package android.processor.compat.changeid;
 
-import com.google.common.annotations.VisibleForTesting;
-
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import java.io.IOException;
 import java.io.OutputStream;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -39,9 +38,11 @@
  * {@code
  * <config>
  *     <compat-change id="111" name="change-name1">
- *         <meta-data definedIn="java.package.ClassName" sourcePosition="java/package/ClassName.java:10" />
+ *         <meta-data definedIn="java.package.ClassName" sourcePosition="java/package/ClassName
+ *         .java:10" />
  *     </compat-change>
- *     <compat-change disabled="true" id="222" loggingOnly= "true" name="change-name2" description="my change">
+ *     <compat-change disabled="true" id="222" loggingOnly= "true" name="change-name2"
+ *     description="my change">
  *         <meta-data .../>
  *     </compat-change>
  *     <compat-change enableAfterTargetSdk="28" id="333" name="change-name3">
@@ -54,10 +55,8 @@
  *
  * The inner {@code meta-data} tags are intended to be stripped before embedding the config on a
  * device. They are intended for use by intermediate build tools only.
- *
  */
-@VisibleForTesting
-public final class XmlWriter {
+final class XmlWriter {
     //XML tags
     private static final String XML_ROOT = "config";
     private static final String XML_CHANGE_ELEMENT = "compat-change";
@@ -74,15 +73,13 @@
     private Document mDocument;
     private Element mRoot;
 
-    @VisibleForTesting
-    public XmlWriter() {
+    XmlWriter() {
         mDocument = createDocument();
         mRoot = mDocument.createElement(XML_ROOT);
         mDocument.appendChild(mRoot);
     }
 
-    @VisibleForTesting
-    public void addChange(Change change) {
+    void addChange(Change change) {
         Element newElement = mDocument.createElement(XML_CHANGE_ELEMENT);
         newElement.setAttribute(XML_NAME_ATTR, change.name);
         newElement.setAttribute(XML_ID_ATTR, change.id.toString());
@@ -111,8 +108,7 @@
         mRoot.appendChild(newElement);
     }
 
-    @VisibleForTesting
-    public void write(OutputStream output) {
+    void write(OutputStream output) throws IOException {
         try {
             TransformerFactory transformerFactory = TransformerFactory.newInstance();
             Transformer transformer = transformerFactory.newTransformer();
@@ -122,7 +118,7 @@
 
             transformer.transform(domSource, result);
         } catch (TransformerException e) {
-            throw new RuntimeException("Failed to write output", e);
+            throw new IOException("Failed to write output", e);
         }
     }
 
diff --git a/java/android/processor/compat/unsupportedappusage/Android.bp b/java/android/processor/compat/unsupportedappusage/Android.bp
index dead2b2..38d35eb 100644
--- a/java/android/processor/compat/unsupportedappusage/Android.bp
+++ b/java/android/processor/compat/unsupportedappusage/Android.bp
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-
 java_library_host {
     name: "unsupportedappusage-annotation-processor-lib",
     srcs: [
@@ -22,6 +21,7 @@
     ],
     static_libs: [
         "guava",
+        "//tools/platform-compat/java/android/processor/compat:compat-processor",
     ],
     openjdk9: {
         javacflags: [
@@ -48,4 +48,9 @@
     ],
 
     use_tools_jar: true,
+
+    visibility: [
+        "//libcore:__pkg__",
+        "//tools/platform-compat/java/android/compat/annotation:__subpackages__",
+    ],
 }
diff --git a/java/android/processor/compat/unsupportedappusage/TEST_MAPPING b/java/android/processor/compat/unsupportedappusage/TEST_MAPPING
deleted file mode 100644
index 6942c03..0000000
--- a/java/android/processor/compat/unsupportedappusage/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "unsupportedappusage-processor-test",
-      "host" : true
-    }
-  ]
-}
diff --git a/java/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessor.java b/java/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessor.java
index 771a34d..3773b6b 100644
--- a/java/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessor.java
+++ b/java/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessor.java
@@ -15,33 +15,26 @@
  */
 package android.processor.compat.unsupportedappusage;
 
+import static javax.tools.Diagnostic.Kind.ERROR;
 import static javax.tools.StandardLocation.CLASS_OUTPUT;
 
+import android.processor.compat.SingleAnnotationProcessor;
+import android.processor.compat.SourcePosition;
+
 import com.google.common.base.Joiner;
-import com.google.common.collect.Iterables;
-import com.sun.source.tree.CompilationUnitTree;
-import com.sun.source.tree.LineMap;
-import com.sun.source.tree.Tree;
-import com.sun.source.util.SourcePositions;
-import com.sun.source.util.TreePath;
-import com.sun.source.util.Trees;
+import com.google.common.collect.Table;
 
 import java.io.IOException;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Messager;
-import javax.annotation.processing.ProcessingEnvironment;
-import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.Nullable;
 import javax.annotation.processing.SupportedAnnotationTypes;
 import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
@@ -49,9 +42,9 @@
 import javax.lang.model.element.AnnotationValue;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
 import javax.lang.model.element.TypeElement;
-import javax.lang.model.util.Types;
-import javax.tools.Diagnostic;
+import javax.tools.FileObject;
 
 /**
  * Annotation processor for {@code UnsupportedAppUsage} annotation.
@@ -62,107 +55,73 @@
  */
 @SupportedAnnotationTypes({"android.compat.annotation.UnsupportedAppUsage"})
 @SupportedSourceVersion(SourceVersion.RELEASE_9)
-public class UnsupportedAppUsageProcessor extends AbstractProcessor {
+public final class UnsupportedAppUsageProcessor extends SingleAnnotationProcessor {
 
-    // Package name for writing output. Output will be written to the "class output" location within
-    // this package.
-    private static final String PACKAGE = "unsupportedappusage";
-    private static final String INDEX_CSV = "unsupportedappusage_index.csv";
+    private static final String GENERATED_INDEX_FILE_EXTENSION = ".uau";
 
     private static final String OVERRIDE_SOURCE_POSITION_PROPERTY = "overrideSourcePosition";
     private static final Pattern OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN = Pattern.compile(
             "^[^:]+:\\d+:\\d+:\\d+:\\d+$");
 
-    private Messager messager;
-    private SourcePositions sourcePositions;
-    private Trees trees;
-    private Types types;
-
-    public synchronized void init(ProcessingEnvironment processingEnv) {
-        super.init(processingEnv);
-
-        this.messager = processingEnv.getMessager();
-        this.trees = Trees.instance(processingEnv);
-        this.types = processingEnv.getTypeUtils();
-
-        this.sourcePositions = trees.getSourcePositions();
-    }
-
     /**
-     * This is the main entry point in the processor, called by the compiler.
+     * CSV header line for the columns returned by {@link #getAnnotationIndex(String, TypeElement,
+     * Element)}.
      */
+    private static final String CSV_HEADER = Joiner.on(',').join(
+            "signature",
+            "file",
+            "startline",
+            "startcol",
+            "endline",
+            "endcol",
+            "properties"
+    );
+
     @Override
-    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
-        if (annotations.size() == 0) {
-            return true;
-        }
-
-        Map<String, Element> signatureMap = new TreeMap<>();
+    protected void process(TypeElement annotation,
+            Table<PackageElement, String, List<Element>> annotatedElements) {
         SignatureConverter signatureConverter = new SignatureConverter(messager);
-        TypeElement annotation = Iterables.getOnlyElement(annotations);
 
-        for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(annotation)) {
-            AnnotationMirror annotationMirror =
-                    getUnsupportedAppUsageAnnotationMirror(annotation, annotatedElement);
-            if (hasElement(annotationMirror, "implicitMember")) {
-                // Implicit member refers to member not present in code, ignore.
-                continue;
-            }
-            String signature = signatureConverter.getSignature(
-                    types, annotation, annotatedElement);
-            if (signature != null) {
-                signatureMap.put(signature, annotatedElement);
+        for (PackageElement packageElement : annotatedElements.rowKeySet()) {
+            Map<String, List<Element>> row = annotatedElements.row(packageElement);
+            for (String enclosingElementName : row.keySet()) {
+                List<String> content = new ArrayList<>();
+                for (Element annotatedElement : row.get(enclosingElementName)) {
+                    String signature = signatureConverter.getSignature(
+                            types, annotation, annotatedElement);
+                    if (signature != null) {
+                        String annotationIndex = getAnnotationIndex(signature, annotation,
+                                annotatedElement);
+                        if (annotationIndex != null) {
+                            content.add(annotationIndex);
+                        }
+                    }
+                }
+
+                if (content.isEmpty()) {
+                    continue;
+                }
+
+                try {
+                    FileObject resource = processingEnv.getFiler().createResource(
+                            CLASS_OUTPUT,
+                            packageElement.toString(),
+                            enclosingElementName + GENERATED_INDEX_FILE_EXTENSION);
+                    try (PrintStream outputStream = new PrintStream(resource.openOutputStream())) {
+                        outputStream.println(CSV_HEADER);
+                        content.forEach(outputStream::println);
+                    }
+                } catch (IOException exception) {
+                    messager.printMessage(ERROR, "Could not write CSV file: " + exception);
+                }
             }
         }
-
-        if (!signatureMap.isEmpty()) {
-            try {
-                writeToFile(INDEX_CSV,
-                        getCsvHeaders(),
-                        signatureMap.entrySet()
-                                .stream()
-                                .map(e -> getAnnotationIndex(e.getKey(), annotation,
-                                        e.getValue())));
-            } catch (IOException e) {
-                throw new RuntimeException("Failed to write output", e);
-            }
-        }
-        return true;
     }
 
-    /**
-     * Write the contents of a stream to a text file, with one line per item.
-     */
-    private void writeToFile(String name,
-            String headerLine,
-            Stream<?> contents) throws IOException {
-        PrintStream out = new PrintStream(processingEnv.getFiler().createResource(
-                CLASS_OUTPUT,
-                PACKAGE,
-                name)
-                .openOutputStream());
-        out.println(headerLine);
-        contents.forEach(o -> out.println(o));
-        if (out.checkError()) {
-            throw new IOException("Error when writing to " + name);
-        }
-        out.close();
-    }
-
-    /**
-     * Returns a CSV header line for the columns returned by
-     * {@link #getAnnotationIndex(String, TypeElement, Element)}.
-     */
-    private String getCsvHeaders() {
-        return Joiner.on(',').join(
-                "signature",
-                "file",
-                "startline",
-                "startcol",
-                "endline",
-                "endcol",
-                "properties"
-        );
+    @Override
+    protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) {
+        // Implicit member refers to member not present in code, ignore.
+        return hasElement(mirror, "implicitMember");
     }
 
     /**
@@ -173,79 +132,46 @@
      * dex-signature,filename,start-line,start-col,end-line,end-col,properties
      *
      * <p>The positions refer to the annotation itself, *not* the annotated member. This can
-     * therefore be used to read just the annotation from the file, and to perform in-place edits on
-     * it.
+     * therefore be used to read just the annotation from the file, and to perform in-place
+     * edits on it.
      *
      * @return A single line of CSV text
      */
+    @Nullable
     private String getAnnotationIndex(String signature, TypeElement annotation, Element element) {
-        AnnotationMirror annotationMirror =
-                getUnsupportedAppUsageAnnotationMirror(annotation, element);
-
+        AnnotationMirror annotationMirror = getSupportedAnnotationMirror(annotation, element);
         String position = getSourcePositionOverride(element, annotationMirror);
         if (position == null) {
-            position = getSourcePosition(element, annotationMirror);
+            SourcePosition sourcePosition = getSourcePosition(element, annotationMirror);
+            if (sourcePosition == null) {
+                return null;
+            }
+            position = Joiner.on(",").join(
+                    sourcePosition.getFilename(),
+                    sourcePosition.getStartLineNumber(),
+                    sourcePosition.getStartColumnNumber(),
+                    sourcePosition.getEndLineNumber(),
+                    sourcePosition.getEndColumnNumber());
         }
         return Joiner.on(",").join(
                 signature,
                 position,
-                getProperties(annotationMirror));
+                getAllProperties(annotationMirror));
     }
 
-    /**
-     * Find the annotation mirror for the @UnsupportedAppUsage annotation on the given element.
-     */
-    private AnnotationMirror getUnsupportedAppUsageAnnotationMirror(TypeElement annotation,
-            Element element) {
-        for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
-            TypeElement type = (TypeElement) mirror.getAnnotationType().asElement();
-            if (types.isSameType(annotation.asType(), mirror.getAnnotationType())) {
-                return mirror;
-            }
-        }
-        return null;
-    }
-
-    private String getSourcePosition(Element element, AnnotationMirror annotationMirror) {
-        TreePath path = trees.getPath(element, annotationMirror);
-        CompilationUnitTree compilationUnit = path.getCompilationUnit();
-        Tree tree = path.getLeaf();
-        long startPosition = sourcePositions.getStartPosition(compilationUnit, tree);
-        long endPosition = sourcePositions.getEndPosition(compilationUnit, tree);
-
-        LineMap lineMap = path.getCompilationUnit().getLineMap();
-        return Joiner.on(",").join(
-                compilationUnit.getSourceFile().getName(),
-                lineMap.getLineNumber(startPosition),
-                lineMap.getColumnNumber(startPosition),
-                lineMap.getLineNumber(endPosition),
-                lineMap.getColumnNumber(endPosition));
-    }
-
-    private String getSourcePositionOverride(Element annotatedElement,
-            AnnotationMirror annotation) {
-        Optional<? extends AnnotationValue> annotationValue =
-                annotation.getElementValues().keySet().stream()
-                        .filter(key -> key.getSimpleName().toString().equals(
-                                OVERRIDE_SOURCE_POSITION_PROPERTY))
-                        .map(key -> annotation.getElementValues().get(key))
-                        .reduce((a, b) -> {
-                            throw new IllegalStateException(
-                                    String.format("Only one %s expected, found %s in %s",
-                                            OVERRIDE_SOURCE_POSITION_PROPERTY, annotation,
-                                            annotatedElement));
-                        });
-
-        if (!annotationValue.isPresent()) {
+    @Nullable
+    private String getSourcePositionOverride(Element element, AnnotationMirror annotation) {
+        AnnotationValue annotationValue =
+                getAnnotationValue(element, annotation, OVERRIDE_SOURCE_POSITION_PROPERTY);
+        if (annotationValue == null) {
             return null;
         }
 
-        String parameterValue = annotationValue.get().getValue().toString();
-
+        String parameterValue = annotationValue.getValue().toString();
         if (!OVERRIDE_SOURCE_POSITION_PROPERTY_PATTERN.matcher(parameterValue).matches()) {
-            messager.printMessage(Diagnostic.Kind.ERROR, String.format(
+            messager.printMessage(ERROR, String.format(
                     "Expected %s to have format string:int:int:int:int",
-                    OVERRIDE_SOURCE_POSITION_PROPERTY), annotatedElement, annotation);
+                    OVERRIDE_SOURCE_POSITION_PROPERTY), element, annotation);
             return null;
         }
 
@@ -257,7 +183,7 @@
                 key -> elementName.equals(key.getSimpleName().toString()));
     }
 
-    private String getProperties(AnnotationMirror annotation) {
+    private String getAllProperties(AnnotationMirror annotation) {
         return annotation.getElementValues().keySet().stream()
                 .filter(key -> !key.getSimpleName().toString().equals(
                         OVERRIDE_SOURCE_POSITION_PROPERTY))
@@ -278,5 +204,4 @@
         }
     }
 
-
 }
diff --git a/javatest/android/processor/compat/changeid/ChangeIdProcessorTest.java b/javatest/android/processor/compat/changeid/ChangeIdProcessorTest.java
index a94a874..dcb47d4 100644
--- a/javatest/android/processor/compat/changeid/ChangeIdProcessorTest.java
+++ b/javatest/android/processor/compat/changeid/ChangeIdProcessorTest.java
@@ -20,18 +20,16 @@
 
 import static javax.tools.StandardLocation.CLASS_OUTPUT;
 
-import com.google.testing.compile.JavaFileObjects;
-import com.google.testing.compile.Compilation;
-import com.google.testing.compile.Compiler;
-import com.google.testing.compile.CompilationSubject;
 import com.google.common.collect.ObjectArrays;
-
-import com.google.common.io.ByteSource;
-
-import javax.tools.JavaFileObject;
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.CompilationSubject;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
 
 import org.junit.Test;
 
+import javax.tools.JavaFileObject;
+
 
 public class ChangeIdProcessorTest {
 
@@ -120,7 +118,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
@@ -168,12 +166,13 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(libcoreExpectedFile);
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "android.util",
-                "SomeClass_compat_config.xml").contentsAsString(UTF_8).isEqualTo(androidExpectedFile);
+                "SomeClass_compat_config.xml").contentsAsString(UTF_8).isEqualTo(
+                androidExpectedFile);
     }
 
     @Test
@@ -204,7 +203,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat.Inner_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
@@ -236,7 +235,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
@@ -269,7 +268,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
@@ -321,8 +320,8 @@
                         .withProcessors(new ChangeIdProcessor())
                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "ChangeId cannot be annotated with both @LoggingOnly and @EnabledAfter or "
-                        + "@Disabled.");
+                "ChangeId cannot be annotated with both @LoggingOnly and "
+                        + "(@EnabledAfter | @Disabled).");
     }
 
     @Test
@@ -346,8 +345,8 @@
                         .withProcessors(new ChangeIdProcessor())
                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "ChangeId cannot be annotated with both @LoggingOnly and @EnabledAfter or "
-                        + "@Disabled.");
+                "ChangeId cannot be annotated with both @LoggingOnly and "
+                        + "(@EnabledAfter | @Disabled).");
     }
 
     @Test
@@ -377,6 +376,7 @@
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
     }
+
     @Test
     public void testIgnoredParams() {
         JavaFileObject[] source = {
@@ -394,7 +394,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
     }
 
@@ -412,10 +412,9 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Non field element changeId annotated with @ChangeId. Got type PARAMETER, "
-                        + "expected FIELD.");
+                "Non FIELD element annotated with @ChangeId.");
     }
 
     @Test
@@ -432,10 +431,9 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Non field element changeId annotated with @ChangeId. Got type PARAMETER, "
-                        + "expected FIELD.");
+                "Non FIELD element annotated with @ChangeId.");
     }
 
     @Test
@@ -455,10 +453,9 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Non constant/final variable MY_CHANGE_ID (or non constant value) annotated with "
-                        + "@ChangeId.");
+                "Non constant/final variable annotated with @ChangeId.");
     }
 
     @Test
@@ -478,9 +475,9 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Variables annotated with @ChangeId should be of type long.");
+                "Variables annotated with @ChangeId must be of type long.");
     }
 
     @Test
@@ -500,9 +497,9 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "Non static variable MY_CHANGE_ID annotated with @ChangeId.");
+                "Non static variable annotated with @ChangeId.");
     }
 
     @Test
@@ -533,7 +530,7 @@
         Compilation compilation =
                 Compiler.javac()
                         .withProcessors(new ChangeIdProcessor())
-                        .compile(ObjectArrays.concat(mAnnotations,source, JavaFileObject.class));
+                        .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
         CompilationSubject.assertThat(compilation).succeeded();
         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
                 "Compat.Inner_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
diff --git a/javatest/android/processor/compat/changeid/TEST_MAPPING b/javatest/android/processor/compat/changeid/TEST_MAPPING
index 27e7926..8b15351 100644
--- a/javatest/android/processor/compat/changeid/TEST_MAPPING
+++ b/javatest/android/processor/compat/changeid/TEST_MAPPING
@@ -1,7 +1,7 @@
 {
   "imports": [
     {
-      "path": "java/android/processor/compat/changeid"
+      "path": "java/android/processor/compat"
     }
   ]
 }
diff --git a/javatest/android/processor/compat/changeid/XmlWriterTest.java b/javatest/android/processor/compat/changeid/XmlWriterTest.java
index a6843d9..f401fc6 100644
--- a/javatest/android/processor/compat/changeid/XmlWriterTest.java
+++ b/javatest/android/processor/compat/changeid/XmlWriterTest.java
@@ -24,7 +24,6 @@
 import java.io.ByteArrayOutputStream;
 import java.io.OutputStream;
 
-
 public class XmlWriterTest {
 
     private static final String HEADER =
@@ -33,7 +32,7 @@
     private OutputStream mOutputStream = new ByteArrayOutputStream();
 
     @Test
-    public void testNoChanges() {
+    public void testNoChanges() throws Exception {
         XmlWriter writer = new XmlWriter();
         writer.write(mOutputStream);
 
@@ -43,7 +42,7 @@
     }
 
     @Test
-    public void testOneChange() {
+    public void testOneChange() throws Exception {
         XmlWriter writer = new XmlWriter();
         Change c = new Change.Builder()
                 .id(123456789L)
@@ -61,7 +60,7 @@
     }
 
     @Test
-    public void testSomeChanges() {
+    public void testSomeChanges() throws Exception {
         XmlWriter writer = new XmlWriter();
         Change c = new Change.Builder()
                 .id(111L)
diff --git a/javatest/android/processor/compat/unsupportedappusage/TEST_MAPPING b/javatest/android/processor/compat/unsupportedappusage/TEST_MAPPING
index 6e4d471..8b15351 100644
--- a/javatest/android/processor/compat/unsupportedappusage/TEST_MAPPING
+++ b/javatest/android/processor/compat/unsupportedappusage/TEST_MAPPING
@@ -1,7 +1,7 @@
 {
   "imports": [
     {
-      "path": "java/android/processor/compat/unsupportedappusage"
+      "path": "java/android/processor/compat"
     }
   ]
 }
diff --git a/javatest/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessorTest.java b/javatest/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessorTest.java
index 51a0822..65b1a2d 100644
--- a/javatest/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessorTest.java
+++ b/javatest/android/processor/compat/unsupportedappusage/UnsupportedAppUsageProcessorTest.java
@@ -26,8 +26,10 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 import javax.tools.JavaFileObject;
 import javax.tools.StandardLocation;
@@ -41,15 +43,21 @@
             "    String expectedSignature() default \"\";\n",
             "    String someProperty() default \"\";",
             "    String overrideSourcePosition() default \"\";",
+            "    String implicitMember() default \"\";",
             "}");
 
-    private CsvReader compileAndReadCsv(JavaFileObject source) throws IOException {
+    private Compilation compile(JavaFileObject source) {
         Compilation compilation =
                 Compiler.javac().withProcessors(new UnsupportedAppUsageProcessor())
                         .compile(ANNOTATION, source);
         CompilationSubject.assertThat(compilation).succeeded();
-        Optional<JavaFileObject> csv = compilation.generatedFile(StandardLocation.CLASS_OUTPUT,
-                "unsupportedappusage/unsupportedappusage_index.csv");
+        return compilation;
+    }
+
+    private CsvReader compileAndReadCsv(JavaFileObject source, String filename) throws IOException {
+        Compilation compilation = compile(source);
+        Optional<JavaFileObject> csv = compilation.generatedFile(
+                StandardLocation.CLASS_OUTPUT, filename);
         assertThat(csv.isPresent()).isTrue();
 
         return new CsvReader(csv.get().openInputStream());
@@ -64,7 +72,7 @@
                 "  @UnsupportedAppUsage",
                 "  public void method() {}",
                 "}");
-        assertThat(compileAndReadCsv(src).getContents().get(0)).containsEntry(
+        assertThat(compileAndReadCsv(src, "a/b/Class.uau").getContents().get(0)).containsEntry(
                 "signature", "La/b/Class;->method()V"
         );
     }
@@ -78,7 +86,7 @@
                 "  @UnsupportedAppUsage", // 4
                 "  public void method() {}", // 5
                 "}");
-        Map<String, String> row = compileAndReadCsv(src).getContents().get(0);
+        Map<String, String> row = compileAndReadCsv(src, "a/b/Class.uau").getContents().get(0);
         assertThat(row).containsEntry("startline", "4");
         assertThat(row).containsEntry("startcol", "3");
         assertThat(row).containsEntry("endline", "4");
@@ -94,7 +102,7 @@
                 "  @UnsupportedAppUsage(someProperty=\"value\")", // 4
                 "  public void method() {}", // 5
                 "}");
-        assertThat(compileAndReadCsv(src).getContents().get(0)).containsEntry(
+        assertThat(compileAndReadCsv(src, "a/b/Class.uau").getContents().get(0)).containsEntry(
                 "properties", "someProperty=%22value%22");
     }
 
@@ -107,7 +115,7 @@
                 "  @UnsupportedAppUsage(overrideSourcePosition=\"otherfile.aidl:30:10:31:20\")",
                 "  public void method() {}", // 5
                 "}");
-        Map<String, String> row = compileAndReadCsv(src).getContents().get(0);
+        Map<String, String> row = compileAndReadCsv(src, "a/b/Class.uau").getContents().get(0);
         assertThat(row).containsEntry("file", "otherfile.aidl");
         assertThat(row).containsEntry("startline", "30");
         assertThat(row).containsEntry("startcol", "10");
@@ -153,6 +161,24 @@
     }
 
     @Test
+    public void testImplicitMemberSkipped() throws Exception {
+        JavaFileObject src = JavaFileObjects.forSourceLines("a.b.Class",
+                "package a.b;", // 1
+                "import android.compat.annotation.UnsupportedAppUsage;", // 2
+                "public class Class {", // 3
+                "  @UnsupportedAppUsage(implicitMember=\"foo\")", // 4
+                "  public void method() {}", // 5
+                "}");
+        List<JavaFileObject> generatedNonClassFiles =
+                compile(src)
+                        .generatedFiles()
+                        .stream()
+                        .filter(file -> !file.getName().endsWith(".class"))
+                        .collect(Collectors.toList());
+        assertThat(generatedNonClassFiles).hasSize(0);
+    }
+
+    @Test
     public void testExpectedSignatureSucceedsIfMatching() throws Exception {
         JavaFileObject src = JavaFileObjects.forSourceLines("a.b.Class",
                 "package a.b;", // 1