Merge "Processor for @UnsupportedAppUsage annotations."
am: e5440456ca
Change-Id: I5e4d53b92c20bf3c34248c6cf4798ab4ba34af7c
diff --git a/Android.bp b/Android.bp
index 70438d2..dda318b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -25,8 +25,8 @@
//
// READ ME: ########################################################
-java_library {
- name: "framework",
+java_defaults {
+ name: "framework-defaults",
installable: true,
srcs: [
@@ -695,12 +695,38 @@
"libmedia2_jni",
],
- javac_shard_size: 150,
-
dxflags: [
"--core-library",
"--multi-dex",
],
+
+}
+
+java_library {
+ name: "framework",
+ defaults: ["framework-defaults"],
+ javac_shard_size: 150,
+}
+
+java_library {
+ name: "framework-annotation-proc",
+ defaults: ["framework-defaults"],
+ // Use UsedByApps annotation processor
+ annotation_processors: ["unsupportedappusage-annotation-processor"],
+ // b/25860419: annotation processors must be explicitly specified for grok
+ annotation_processor_classes: [
+ "android.processor.unsupportedappusage.UsedByAppsProcessor",
+ ],
+}
+
+// A host library including just UnsupportedAppUsage.java so that the annotation
+// processor can also use this annotation.
+java_library_host {
+ name: "unsupportedappusage-annotation",
+ srcs: [
+ "core/java/android/annotation/IntDef.java",
+ "core/java/android/annotation/UnsupportedAppUsage.java",
+ ],
}
// A temporary build target that is conditionally included on the bootclasspath if
diff --git a/tools/processors/unsupportedappusage/Android.bp b/tools/processors/unsupportedappusage/Android.bp
new file mode 100644
index 0000000..98f3c95
--- /dev/null
+++ b/tools/processors/unsupportedappusage/Android.bp
@@ -0,0 +1,15 @@
+
+java_library_host {
+ name: "unsupportedappusage-annotation-processor",
+ java_resources: [
+ "META-INF/**/*",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ static_libs: [
+ "guava",
+ "unsupportedappusage-annotation"
+ ],
+ use_tools_jar: true,
+}
diff --git a/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor b/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..4a969d3
--- /dev/null
+++ b/tools/processors/unsupportedappusage/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+android.processor.unsupportedappusage.UnsupportedAppUsageProcessor
diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java
new file mode 100644
index 0000000..ef29146
--- /dev/null
+++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/SignatureBuilder.java
@@ -0,0 +1,228 @@
+/*
+ * 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 android.processor.unsupportedappusage;
+
+import static javax.lang.model.element.ElementKind.PACKAGE;
+import static javax.tools.Diagnostic.Kind.ERROR;
+import static javax.tools.Diagnostic.Kind.WARNING;
+
+import android.annotation.UnsupportedAppUsage;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.sun.tools.javac.code.Type;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.processing.Messager;
+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.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Builds a dex signature for a given method or field.
+ */
+public class SignatureBuilder {
+
+ private static final Map<TypeKind, String> TYPE_MAP = ImmutableMap.<TypeKind, String>builder()
+ .put(TypeKind.BOOLEAN, "Z")
+ .put(TypeKind.BYTE, "B")
+ .put(TypeKind.CHAR, "C")
+ .put(TypeKind.DOUBLE, "D")
+ .put(TypeKind.FLOAT, "F")
+ .put(TypeKind.INT, "I")
+ .put(TypeKind.LONG, "J")
+ .put(TypeKind.SHORT, "S")
+ .put(TypeKind.VOID, "V")
+ .build();
+
+ private final Messager mMessager;
+
+ /**
+ * Exception used internally when we can't build a signature. Whenever this is thrown, an error
+ * will also be written to the Messager.
+ */
+ private class SignatureBuilderException extends Exception {
+ public SignatureBuilderException(String message) {
+ super(message);
+ }
+ public void report(Element offendingElement) {
+ mMessager.printMessage(ERROR, getMessage(), offendingElement);
+ }
+ }
+
+ public SignatureBuilder(Messager messager) {
+ mMessager = messager;
+ }
+
+ /**
+ * Returns a list of enclosing elements for the given element, with the package first, and
+ * excluding the element itself.
+ */
+ private List<Element> getEnclosingElements(Element e) {
+ List<Element> enclosing = new ArrayList<>();
+ e = e.getEnclosingElement(); // don't include the element itself.
+ while (e != null) {
+ enclosing.add(e);
+ e = e.getEnclosingElement();
+ }
+ Collections.reverse(enclosing);
+ return enclosing;
+ }
+
+ /**
+ * Get the dex signature for a clazz, in format "Lpackage/name/Outer$Inner;"
+ */
+ private String getClassSignature(TypeElement clazz) {
+ StringBuilder sb = new StringBuilder("L");
+ for (Element enclosing : getEnclosingElements(clazz)) {
+ if (enclosing.getKind() == PACKAGE) {
+ sb.append(((PackageElement) enclosing)
+ .getQualifiedName()
+ .toString()
+ .replace('.', '/'));
+ sb.append('/');
+ } else {
+ sb.append(enclosing.getSimpleName()).append('$');
+ }
+
+ }
+ return sb
+ .append(clazz.getSimpleName())
+ .append(";")
+ .toString();
+ }
+
+ /**
+ * Returns the type signature for a given type. For primitive types, a single character.
+ * For classes, the class signature. For arrays, a "[" preceeding the component type.
+ */
+ private String getTypeSignature(TypeMirror type) throws SignatureBuilderException {
+ String sig = TYPE_MAP.get(type.getKind());
+ if (sig != null) {
+ return sig;
+ }
+ switch (type.getKind()) {
+ case ARRAY:
+ return "[" + getTypeSignature(((ArrayType) type).getComponentType());
+ case DECLARED:
+ Element declaring = ((DeclaredType) type).asElement();
+ if (!(declaring instanceof TypeElement)) {
+ throw new SignatureBuilderException(
+ "Can't handle declared type of kind " + declaring.getKind());
+ }
+ return getClassSignature((TypeElement) declaring);
+ case TYPEVAR:
+ Type.TypeVar typeVar = (Type.TypeVar) type;
+ if (typeVar.getLowerBound().getKind() != TypeKind.NULL) {
+ return getTypeSignature(typeVar.getLowerBound());
+ } else if (typeVar.getUpperBound().getKind() != TypeKind.NULL) {
+ return getTypeSignature(typeVar.getUpperBound());
+ } else {
+ throw new SignatureBuilderException("Can't handle typevar with no bound");
+ }
+
+ default:
+ throw new SignatureBuilderException("Can't handle type of kind " + type.getKind());
+ }
+ }
+
+ /**
+ * Get the signature for an executable, either a method or a constructor.
+ *
+ * @param name "<init>" for constructor, else the method name
+ * @param method The executable element in question.
+ */
+ private String getExecutableSignature(CharSequence name, ExecutableElement method)
+ throws SignatureBuilderException {
+ StringBuilder sig = new StringBuilder();
+ sig.append(getClassSignature((TypeElement) method.getEnclosingElement()))
+ .append("->")
+ .append(name)
+ .append("(");
+ for (VariableElement param : method.getParameters()) {
+ sig.append(getTypeSignature(param.asType()));
+ }
+ sig.append(")")
+ .append(getTypeSignature(method.getReturnType()));
+ return sig.toString();
+ }
+
+ private String buildMethodSignature(ExecutableElement method) throws SignatureBuilderException {
+ return getExecutableSignature(method.getSimpleName(), method);
+ }
+
+ private String buildConstructorSignature(ExecutableElement cons)
+ throws SignatureBuilderException {
+ return getExecutableSignature("<init>", cons);
+ }
+
+ private String buildFieldSignature(VariableElement field) throws SignatureBuilderException {
+ StringBuilder sig = new StringBuilder();
+ sig.append(getClassSignature((TypeElement) field.getEnclosingElement()))
+ .append("->")
+ .append(field.getSimpleName())
+ .append(":")
+ .append(getTypeSignature(field.asType()))
+ ;
+ return sig.toString();
+ }
+
+ public String buildSignature(Element element) {
+ UnsupportedAppUsage uba = element.getAnnotation(UnsupportedAppUsage.class);
+ try {
+ String signature;
+ switch (element.getKind()) {
+ case METHOD:
+ signature = buildMethodSignature((ExecutableElement) element);
+ break;
+ case CONSTRUCTOR:
+ signature = buildConstructorSignature((ExecutableElement) element);
+ break;
+ case FIELD:
+ signature = buildFieldSignature((VariableElement) element);
+ break;
+ default:
+ return null;
+ }
+ // if we have an expected signature on the annotation, warn if it doesn't match.
+ if (!Strings.isNullOrEmpty(uba.expectedSignature())) {
+ if (!signature.equals(uba.expectedSignature())) {
+ mMessager.printMessage(
+ WARNING,
+ String.format("Expected signature doesn't match generated signature.\n"
+ + " Expected: %s\n Generated: %s",
+ uba.expectedSignature(), signature),
+ element);
+ }
+ }
+ return signature;
+ } catch (SignatureBuilderException problem) {
+ problem.report(element);
+ return null;
+ }
+ }
+}
diff --git a/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java
new file mode 100644
index 0000000..1d4c435
--- /dev/null
+++ b/tools/processors/unsupportedappusage/src/android/processor/unsupportedappusage/UnsupportedAppUsageProcessor.java
@@ -0,0 +1,173 @@
+/*
+ * 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 android.processor.unsupportedappusage;
+
+import static javax.tools.StandardLocation.CLASS_OUTPUT;
+
+import android.annotation.UnsupportedAppUsage;
+
+import com.google.common.base.Joiner;
+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 java.io.IOException;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+
+/**
+ * Annotation processor for {@link UnsupportedAppUsage} annotations.
+ *
+ * This processor currently outputs two things:
+ * 1. A greylist.txt containing dex signatures of all annotated elements.
+ * 2. A CSV file with a mapping of dex signatures to corresponding source positions.
+ *
+ * The first will be used at a later stage of the build to add access flags to the dex file. The
+ * second is used for automating updates to the annotations themselves.
+ */
+@SupportedAnnotationTypes({"android.annotation.UnsupportedAppUsage"})
+public class UnsupportedAppUsageProcessor extends AbstractProcessor {
+
+ // 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";
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Find the annotation mirror for the @UnsupportedAppUsage annotation on the given element.
+ */
+ private AnnotationMirror getUnsupportedAppUsageAnnotationMirror(Element e) {
+ for (AnnotationMirror m : e.getAnnotationMirrors()) {
+ TypeElement type = (TypeElement) m.getAnnotationType().asElement();
+ if (type.getQualifiedName().toString().equals(
+ UnsupportedAppUsage.class.getCanonicalName())) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a CSV header line for the columns returned by
+ * {@link #getAnnotationIndex(String, Element)}.
+ */
+ private String getCsvHeaders() {
+ return Joiner.on(',').join(
+ "signature",
+ "file",
+ "startline",
+ "startcol",
+ "endline",
+ "endcol"
+ );
+ }
+
+ /**
+ * Maps an annotated element to the source position of the @UnsupportedAppUsage annotation
+ * attached to it. It returns CSV in the format:
+ * dex-signature,filename,start-line,start-col,end-line,end-col
+ *
+ * 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.
+ *
+ * @param signature the dex signature for the element.
+ * @param annotatedElement The annotated element
+ * @return A single line of CSV text
+ */
+ private String getAnnotationIndex(String signature, Element annotatedElement) {
+ JavacElements javacElem = (JavacElements) processingEnv.getElementUtils();
+ AnnotationMirror unsupportedAppUsage =
+ getUnsupportedAppUsageAnnotationMirror(annotatedElement);
+ Pair<JCTree, JCTree.JCCompilationUnit> pair =
+ javacElem.getTreeAndTopLevel(annotatedElement, unsupportedAppUsage, null);
+ Position.LineMap lines = pair.snd.lineMap;
+ return Joiner.on(",").join(
+ signature,
+ pair.snd.getSourceFile().getName(),
+ lines.getLineNumber(pair.fst.pos().getStartPosition()),
+ lines.getColumnNumber(pair.fst.pos().getStartPosition()),
+ lines.getLineNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions)),
+ lines.getColumnNumber(pair.fst.pos().getEndPosition(pair.snd.endPositions)));
+ }
+
+ /**
+ * 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> annotated = roundEnv.getElementsAnnotatedWith(
+ UnsupportedAppUsage.class);
+ if (annotated.size() == 0) {
+ return true;
+ }
+ // build signatures for each annotated member, and put them in a map of signature to member
+ Map<String, Element> signatureMap = new TreeMap<>();
+ SignatureBuilder sb = new SignatureBuilder(processingEnv.getMessager());
+ for (Element e : annotated) {
+ String sig = sb.buildSignature(e);
+ if (sig != null) {
+ signatureMap.put(sig, e);
+ }
+ }
+ try {
+ writeToFile(INDEX_CSV,
+ getCsvHeaders(),
+ signatureMap.entrySet()
+ .stream()
+ .map(e -> getAnnotationIndex(e.getKey() ,e.getValue())));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write output", e);
+ }
+ return true;
+ }
+}