diff options
| author | 2018-12-13 18:06:30 -0500 | |
|---|---|---|
| committer | 2019-01-18 16:47:29 -0500 | |
| commit | c1a4decb3945604583badf4b5eb5b5f0c6c39375 (patch) | |
| tree | 7b8fe72bac7459acd84382def4aaf772cfdafbd8 | |
| parent | 61ae7ed2c6697d95c06422cad6aa4d4063dd4b24 (diff) | |
Annotation processing for @InspectableProperty
Test: atest --host view-inspector-annotation-processor-test
Bug: 117616612
Change-Id: Ia9641b4efae5f6945084849309f81f2d31faf2ec
16 files changed, 622 insertions, 80 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index fbba73c46ab4..08531829cba1 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2124,6 +2124,33 @@ package android.view.inputmethod { } +package android.view.inspector { + + public abstract class InspectableNodeName implements java.lang.annotation.Annotation { + } + + public abstract class InspectableProperty implements java.lang.annotation.Annotation { + } + + public static abstract class InspectableProperty.EnumMap implements java.lang.annotation.Annotation { + } + + public static abstract class InspectableProperty.FlagMap implements java.lang.annotation.Annotation { + } + + public static final class InspectableProperty.ValueType extends java.lang.Enum { + method public static android.view.inspector.InspectableProperty.ValueType valueOf(java.lang.String); + method public static final android.view.inspector.InspectableProperty.ValueType[] values(); + enum_constant public static final android.view.inspector.InspectableProperty.ValueType COLOR; + enum_constant public static final android.view.inspector.InspectableProperty.ValueType GRAVITY; + enum_constant public static final android.view.inspector.InspectableProperty.ValueType INFERRED; + enum_constant public static final android.view.inspector.InspectableProperty.ValueType INT_ENUM; + enum_constant public static final android.view.inspector.InspectableProperty.ValueType INT_FLAG; + enum_constant public static final android.view.inspector.InspectableProperty.ValueType NONE; + } + +} + package android.widget { public abstract class AbsListView extends android.widget.AdapterView implements android.widget.Filter.FilterListener android.text.TextWatcher android.view.ViewTreeObserver.OnGlobalLayoutListener android.view.ViewTreeObserver.OnTouchModeChangeListener { diff --git a/core/java/android/view/inspector/InspectableNodeName.java b/core/java/android/view/inspector/InspectableNodeName.java index ea94ad4c5df8..7b9a507ee45d 100644 --- a/core/java/android/view/inspector/InspectableNodeName.java +++ b/core/java/android/view/inspector/InspectableNodeName.java @@ -19,6 +19,8 @@ package android.view.inspector; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.annotation.TestApi; + import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -39,6 +41,7 @@ import java.lang.annotation.Target; */ @Target({TYPE}) @Retention(SOURCE) +@TestApi public @interface InspectableNodeName { /** * The display name for nodes of this type. diff --git a/core/java/android/view/inspector/InspectableProperty.java b/core/java/android/view/inspector/InspectableProperty.java index a57470c7c908..355ff1d85e1f 100644 --- a/core/java/android/view/inspector/InspectableProperty.java +++ b/core/java/android/view/inspector/InspectableProperty.java @@ -20,6 +20,7 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.annotation.TestApi; import android.content.res.Resources; import java.lang.annotation.Retention; @@ -40,6 +41,7 @@ import java.lang.annotation.Target; */ @Target({METHOD}) @Retention(SOURCE) +@TestApi public @interface InspectableProperty { /** * The name of the property. @@ -86,7 +88,6 @@ public @interface InspectableProperty { * * @return An array of {@link EnumMap}, empty if not applicable * @see android.annotation.IntDef - * @see IntEnumMapping */ EnumMap[] enumMapping() default {}; @@ -109,6 +110,7 @@ public @interface InspectableProperty { */ @Target({TYPE}) @Retention(SOURCE) + @TestApi @interface EnumMap { /** * The string name of this enumeration value. @@ -133,6 +135,7 @@ public @interface InspectableProperty { */ @Target({TYPE}) @Retention(SOURCE) + @TestApi @interface FlagMap { /** * The string name of this flag. @@ -167,15 +170,22 @@ public @interface InspectableProperty { * * @hide */ + @TestApi enum ValueType { /** * No special handling, property is considered to be a numeric value. + * + * @hide */ + @TestApi NONE, /** * The default the annotation processor infers the value type from context. + * + * @hide */ + @TestApi INFERRED, /** @@ -184,7 +194,9 @@ public @interface InspectableProperty { * This is inferred if {@link #enumMapping()} is specified. * * @see EnumMap + * @hide */ + @TestApi INT_ENUM, /** @@ -193,7 +205,9 @@ public @interface InspectableProperty { * This is inferred if {@link #flagMapping()} is specified. * * @see FlagMap + * @hide */ + @TestApi INT_FLAG, /** @@ -203,7 +217,9 @@ public @interface InspectableProperty { * {@link android.annotation.ColorLong} on the getter method. * * @see android.graphics.Color + * @hide */ + @TestApi COLOR, /** @@ -212,7 +228,9 @@ public @interface InspectableProperty { * This type is not inferred, and is non-trivial to represent using {@link FlagMap}. * * @see android.view.Gravity + * @hide */ + @TestApi GRAVITY } } diff --git a/tools/processors/view_inspector/Android.bp b/tools/processors/view_inspector/Android.bp index ca6b3c4572f5..9b5df56e3987 100644 --- a/tools/processors/view_inspector/Android.bp +++ b/tools/processors/view_inspector/Android.bp @@ -5,7 +5,7 @@ java_library_host { java_resource_dirs: ["src/resources"], static_libs: [ - "javapoet", + "javapoet", ], use_tools_jar: true, @@ -18,9 +18,9 @@ java_test_host { java_resource_dirs: ["test/resources"], static_libs: [ - "guava", "junit", - "view-inspector-annotation-processor", + "guava", + "view-inspector-annotation-processor" ], test_suites: ["general-tests"], diff --git a/tools/processors/view_inspector/TEST_MAPPING b/tools/processors/view_inspector/TEST_MAPPING index a91b5b452c39..b4c9cab6916e 100644 --- a/tools/processors/view_inspector/TEST_MAPPING +++ b/tools/processors/view_inspector/TEST_MAPPING @@ -2,6 +2,8 @@ "presubmit": [ { "name": "view-inspector-annotation-processor-test" + }, { + "name": "CtsViewInspectorAnnotationProcessorTestCases" } ] } diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java index f157949f4d1b..fc4cd01a5a2a 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -24,6 +24,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -78,40 +79,126 @@ final class AnnotationUtils { } /** - * Extract a string-valued property from an {@link AnnotationMirror}. + * Determine if an annotation with the supplied qualified name is present on the element. * - * @param propertyName The name of the requested property - * @param annotationMirror The mirror to search for the property - * @return The String value of the annotation or null + * @param element The element to check for the presence of an annotation + * @param annotationQualifiedName The name of the annotation to check for + * @return True if the annotation is present, false otherwise */ - Optional<String> stringProperty(String propertyName, AnnotationMirror annotationMirror) { - final AnnotationValue value = valueByName(propertyName, annotationMirror); - if (value != null) { - return Optional.of((String) value.getValue()); - } else { - return Optional.empty(); + boolean hasAnnotation(Element element, String annotationQualifiedName) { + final TypeElement namedElement = mElementUtils.getTypeElement(annotationQualifiedName); + + if (namedElement != null) { + final TypeMirror annotationType = namedElement.asType(); + + for (AnnotationMirror annotation : element.getAnnotationMirrors()) { + if (mTypeUtils.isSubtype(annotation.getAnnotationType(), annotationType)) { + return true; + } + } } + + return false; } + /** + * Get the typed value of an annotation property by name. + * + * The returned optional will be empty if the value was left at the default, or if the value + * of the property is null. + * + * @param propertyName The name of the property to search for + * @param valueClass The expected class of the property value + * @param element The element the annotation is on, used for exceptions + * @param annotationMirror An annotation mirror to search for the property + * @param <T> The type of the value + * @return An optional containing the typed value of the named property + */ + <T> Optional<T> typedValueByName( + String propertyName, + Class<T> valueClass, + Element element, + AnnotationMirror annotationMirror) { + return valueByName(propertyName, annotationMirror).map(annotationValue -> { + final Object value = annotationValue.getValue(); + + if (value == null) { + throw new ProcessingException( + String.format( + "Unexpected null value for annotation property \"%s\".", + propertyName), + element, + annotationMirror, + annotationValue); + } + + if (valueClass.isAssignableFrom(value.getClass())) { + return valueClass.cast(value); + } else { + throw new ProcessingException( + String.format( + "Expected annotation property \"%s\" to have type %s, but got %s.", + propertyName, + valueClass.getCanonicalName(), + value.getClass().getCanonicalName()), + element, + annotationMirror, + annotationValue); + } + }); + } + + /** + * Get the untyped value of an annotation property by name. + * + * The returned optional will be empty if the value was left at the default, or if the value + * of the property is null. + * + * @param propertyName The name of the property to search for + * @param element The element the annotation is on, used for exceptions + * @param annotationMirror An annotation mirror to search for the property + * @return An optional containing the untyped value of the named property + * @see AnnotationValue#getValue() + */ + Optional<Object> untypedValueByName( + String propertyName, + Element element, + AnnotationMirror annotationMirror) { + return valueByName(propertyName, annotationMirror).map(annotationValue -> { + final Object value = annotationValue.getValue(); + + if (value == null) { + throw new ProcessingException( + String.format( + "Unexpected null value for annotation property \"%s\".", + propertyName), + element, + annotationMirror, + annotationValue); + } + + return value; + }); + } /** * Extract a {@link AnnotationValue} from a mirror by string property name. * * @param propertyName The name of the property requested property - * @param annotationMirror - * @return + * @param annotationMirror The mirror to search for the property + * @return The value of the property */ - AnnotationValue valueByName(String propertyName, AnnotationMirror annotationMirror) { + Optional<AnnotationValue> valueByName(String propertyName, AnnotationMirror annotationMirror) { final Map<? extends ExecutableElement, ? extends AnnotationValue> valueMap = annotationMirror.getElementValues(); for (ExecutableElement method : valueMap.keySet()) { if (method.getSimpleName().contentEquals(propertyName)) { - return valueMap.get(method); + return Optional.ofNullable(valueMap.get(method)); } } - return null; + // Property not explicitly defined, use default value. + return Optional.empty(); } - } diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java index 579745d2aaef..f1ebb87fed4d 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -21,6 +21,7 @@ import com.squareup.javapoet.ClassName; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; /** @@ -70,7 +71,7 @@ public final class InspectableClassModel { * @return The property or an empty optional */ public Optional<Property> getProperty(String name) { - return Optional.of(mPropertyMap.get(name)); + return Optional.ofNullable(mPropertyMap.get(name)); } /** @@ -87,13 +88,15 @@ public final class InspectableClassModel { */ public static final class Property { private final String mName; - private String mGetter; - private Type mType; + private final String mGetter; + private final Type mType; private boolean mAttributeIdInferrableFromR = true; private int mAttributeId = 0; - public Property(String name) { - mName = name; + public Property(String name, String getter, Type type) { + mName = Objects.requireNonNull(name, "Name must not be null"); + mGetter = Objects.requireNonNull(getter, "Getter must not be null"); + mType = Objects.requireNonNull(type, "Type must not be null"); } public int getAttributeId() { @@ -126,18 +129,10 @@ public final class InspectableClassModel { return mGetter; } - public void setGetter(String getter) { - mGetter = getter; - } - public Type getType() { return mType; } - public void setType(Type type) { - mType = type; - } - public enum Type { /** Primitive or boxed {@code boolean} */ BOOLEAN, diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java index a186a82af160..46819b28c1e8 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -23,7 +23,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; /** - * Process {InspectableNodeName} annotations + * Process {@code @InspectableNodeName} annotations. * * @see android.view.inspector.InspectableNodeName */ @@ -58,7 +58,8 @@ public final class InspectableNodeNameProcessor implements ModelProcessor { try { final AnnotationMirror mirror = mAnnotationUtils.exactlyOneMirror(mQualifiedName, element); - final Optional<String> nodeName = mAnnotationUtils.stringProperty("value", mirror); + final Optional<String> nodeName = mAnnotationUtils + .typedValueByName("value", String.class, element, mirror); if (!model.getNodeName().isPresent() || model.getNodeName().equals(nodeName)) { model.setNodeName(nodeName); diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectablePropertyProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectablePropertyProcessor.java new file mode 100644 index 000000000000..f666be7a2a61 --- /dev/null +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectablePropertyProcessor.java @@ -0,0 +1,408 @@ +/* + * Copyright 2019 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.view.inspector; + +import android.processor.view.inspector.InspectableClassModel.Property; + +import java.util.Set; +import java.util.regex.Pattern; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +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.TypeElement; +import javax.lang.model.type.NoType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Process {@code @InspectableProperty} annotations. + * + * @see android.view.inspector.InspectableProperty + */ +public final class InspectablePropertyProcessor implements ModelProcessor { + private final String mQualifiedName; + private final ProcessingEnvironment mProcessingEnv; + private final AnnotationUtils mAnnotationUtils; + + /** + * Regex that matches methods names of the form {@code #getValue()}. + */ + private static final Pattern GETTER_GET_PREFIX = Pattern.compile("\\Aget[A-Z]"); + + /** + * Regex that matches method name of the form {@code #isPredicate()}. + */ + private static final Pattern GETTER_IS_PREFIX = Pattern.compile("\\Ais[A-Z]"); + + /** + * Set of android and androidx annotation qualified names for colors packed into {@code int}. + * + * @see android.annotation.ColorInt + */ + private static final String[] COLOR_INT_ANNOTATION_NAMES = { + "android.annotation.ColorInt", + "androidx.annotation.ColorInt"}; + + /** + * Set of android and androidx annotation qualified names for colors packed into {@code long}. + * @see android.annotation.ColorLong + */ + private static final String[] COLOR_LONG_ANNOTATION_NAMES = { + "android.annotation.ColorLong", + "androidx.annotation.ColorLong"}; + + /** + * @param annotationQualifiedName The qualified name of the annotation to process + * @param processingEnv The processing environment from the parent processor + */ + public InspectablePropertyProcessor( + String annotationQualifiedName, + ProcessingEnvironment processingEnv) { + mQualifiedName = annotationQualifiedName; + mProcessingEnv = processingEnv; + mAnnotationUtils = new AnnotationUtils(processingEnv); + } + + @Override + public void process(Element element, InspectableClassModel model) { + try { + final AnnotationMirror annotation = + mAnnotationUtils.exactlyOneMirror(mQualifiedName, element); + final ExecutableElement getter = ensureGetter(element); + final Property property = buildProperty(getter, annotation); + + model.getProperty(property.getName()).ifPresent(p -> { + throw new ProcessingException( + String.format( + "Property \"%s\" is already defined on #%s().", + p.getName(), + p.getGetter()), + getter, + annotation); + }); + + model.putProperty(property); + } catch (ProcessingException processingException) { + processingException.print(mProcessingEnv.getMessager()); + } + } + + /** + * Check that an element is shaped like a getter. + * + * @param element An element that hopefully represents a getter + * @throws ProcessingException if the element isn't a getter + * @return An {@link ExecutableElement} that represents a getter method. + */ + private ExecutableElement ensureGetter(Element element) { + if (element.getKind() != ElementKind.METHOD) { + throw new ProcessingException( + String.format("Expected a method, got a %s", element.getKind()), + element); + } + + final ExecutableElement method = (ExecutableElement) element; + final Set<Modifier> modifiers = method.getModifiers(); + + if (modifiers.contains(Modifier.PRIVATE)) { + throw new ProcessingException( + "Property getter methods must not be private.", + element); + } + + if (modifiers.contains(Modifier.ABSTRACT)) { + throw new ProcessingException( + "Property getter methods must not be abstract.", + element); + } + + if (modifiers.contains(Modifier.STATIC)) { + throw new ProcessingException( + "Property getter methods must not be static.", + element); + } + + if (!method.getParameters().isEmpty()) { + throw new ProcessingException( + String.format( + "Expected a getter method to take no parameters, " + + "but got %d parameters.", + method.getParameters().size()), + element); + } + + if (method.isVarArgs()) { + throw new ProcessingException( + "Expected a getter method to take no arguments, but got a var args method.", + element); + } + + if (method.getReturnType() instanceof NoType) { + throw new ProcessingException( + "Expected a getter to have a return type, got void.", + element); + } + + return method; + } + + /** + * Build a {@link Property} from a getter and an inspectable property annotation. + * + * @param getter An element representing the getter to build from + * @param annotation A mirror of an inspectable property-shaped annotation + * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled + * @return A property for the getter and annotation + */ + private Property buildProperty(ExecutableElement getter, AnnotationMirror annotation) { + final String name = mAnnotationUtils + .typedValueByName("name", String.class, getter, annotation) + .orElseGet(() -> inferPropertyNameFromGetter(getter)); + + final Property property = new Property( + name, + getter.getSimpleName().toString(), + determinePropertyType(getter, annotation)); + + mAnnotationUtils + .typedValueByName("hasAttributeId", Boolean.class, getter, annotation) + .ifPresent(property::setAttributeIdInferrableFromR); + + mAnnotationUtils + .typedValueByName("attributeId", Integer.class, getter, annotation) + .ifPresent(property::setAttributeId); + + return property; + } + + /** + * Determine the property type from the annotation, return type, or context clues. + * + * @param getter An element representing the getter to build from + * @param annotation A mirror of an inspectable property-shaped annotation + * @return The resolved property type + * @throws ProcessingException If the property type cannot be resolved + * @see android.view.inspector.InspectableProperty#valueType() + */ + private Property.Type determinePropertyType( + ExecutableElement getter, + AnnotationMirror annotation) { + + final String valueType = mAnnotationUtils + .untypedValueByName("valueType", getter, annotation) + .map(Object::toString) + .orElse("INFERRED"); + + final Property.Type returnType = convertReturnTypeToPropertyType(getter); + + switch (valueType) { + case "INFERRED": + if (hasColorAnnotation(getter)) { + return Property.Type.COLOR; + } else { + return returnType; + } + case "NONE": + return returnType; + case "COLOR": + switch (returnType) { + case COLOR: + case INT: + case LONG: + return Property.Type.COLOR; + default: + throw new ProcessingException( + "Color must be a long, integer, or android.graphics.Color", + getter, + annotation); + } + case "GRAVITY": + if (returnType == Property.Type.INT) { + return Property.Type.GRAVITY; + } else { + throw new ProcessingException( + String.format("Gravity must be an integer, got %s", returnType), + getter, + annotation); + } + case "INT_ENUM": + case "INT_FLAG": + throw new ProcessingException("Not implemented", getter, annotation); + default: + throw new ProcessingException( + String.format("Unknown value type enumeration value: %s", valueType), + getter, + annotation); + } + } + + /** + * Get a property type from the return type of a getter. + * + * @param getter The getter to extract the return type of + * @throws ProcessingException If the return type is not a primitive or an object + * @return The property type returned by the getter + */ + private Property.Type convertReturnTypeToPropertyType(ExecutableElement getter) { + final TypeMirror returnType = getter.getReturnType(); + + switch (unboxType(returnType)) { + case BOOLEAN: + return Property.Type.BOOLEAN; + case BYTE: + return Property.Type.BYTE; + case CHAR: + return Property.Type.CHAR; + case DOUBLE: + return Property.Type.DOUBLE; + case FLOAT: + return Property.Type.FLOAT; + case INT: + return Property.Type.INT; + case LONG: + return Property.Type.LONG; + case SHORT: + return Property.Type.SHORT; + case DECLARED: + if (isColorType(returnType)) { + return Property.Type.COLOR; + } else { + return Property.Type.OBJECT; + } + default: + throw new ProcessingException( + String.format("Unsupported return type %s.", returnType), + getter); + } + } + + /** + * Determine if a getter is annotated with color annotation matching its return type. + * + * Note that an {@code int} return value annotated with {@link android.annotation.ColorLong} is + * not considered to be annotated, nor is a {@code long} annotated with + * {@link android.annotation.ColorInt}. + * + * @param getter The getter to query + * @return True if the getter has a color annotation, false otherwise + * + */ + private boolean hasColorAnnotation(ExecutableElement getter) { + switch (unboxType(getter.getReturnType())) { + case INT: + for (String name : COLOR_INT_ANNOTATION_NAMES) { + if (mAnnotationUtils.hasAnnotation(getter, name)) { + return true; + } + } + return false; + case LONG: + for (String name : COLOR_LONG_ANNOTATION_NAMES) { + if (mAnnotationUtils.hasAnnotation(getter, name)) { + return true; + } + } + return false; + default: + return false; + } + } + + /** + * Infer a property name from a getter method. + * + * If the method is prefixed with {@code get}, the prefix will be stripped, and the + * capitalization fixed. E.g.: {@code getSomeProperty} to {@code someProperty}. + * + * Additionally, if the method's return type is a boolean, an {@code is} prefix will also be + * stripped. E.g.: {@code isPropertyEnabled} to {@code propertyEnabled}. + * + * Failing that, this method will just return the full name of the getter. + * + * @param getter An element representing a getter + * @return A string property name + */ + private String inferPropertyNameFromGetter(ExecutableElement getter) { + final String name = getter.getSimpleName().toString(); + + if (GETTER_GET_PREFIX.matcher(name).find()) { + return name.substring(3, 4).toLowerCase() + name.substring(4); + } else if (isBoolean(getter.getReturnType()) && GETTER_IS_PREFIX.matcher(name).find()) { + return name.substring(2, 3).toLowerCase() + name.substring(3); + } else { + return name; + } + } + + /** + * Determine if a {@link TypeMirror} is a boxed or unboxed boolean. + * + * @param type The type mirror to check + * @return True if the type is a boolean + */ + private boolean isBoolean(TypeMirror type) { + if (type.getKind() == TypeKind.DECLARED) { + return mProcessingEnv.getTypeUtils().unboxedType(type).getKind() == TypeKind.BOOLEAN; + } else { + return type.getKind() == TypeKind.BOOLEAN; + } + } + + /** + * Unbox a type mirror if it represents a boxed type, otherwise pass it through. + * + * @param typeMirror The type mirror to unbox + * @return The same type mirror, or an unboxed primitive version + */ + private TypeKind unboxType(TypeMirror typeMirror) { + final TypeKind typeKind = typeMirror.getKind(); + + if (typeKind.isPrimitive()) { + return typeKind; + } else if (typeKind == TypeKind.DECLARED) { + try { + return mProcessingEnv.getTypeUtils().unboxedType(typeMirror).getKind(); + } catch (IllegalArgumentException e) { + return typeKind; + } + } else { + return typeKind; + } + } + + /** + * Determine if a type mirror represents a subtype of {@link android.graphics.Color}. + * + * @param typeMirror The type mirror to test + * @return True if it represents a subclass of color, false otherwise + */ + private boolean isColorType(TypeMirror typeMirror) { + final TypeElement colorType = mProcessingEnv + .getElementUtils() + .getTypeElement("android.graphics.Color"); + + if (colorType == null) { + return false; + } else { + return mProcessingEnv.getTypeUtils().isSubtype(typeMirror, colorType.asType()); + } + } +} diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java index 8b2cc847c0b3..dd4d8f54fb68 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -51,12 +51,6 @@ public final class InspectionCompanionGenerator { private static final ClassName R_CLASS_NAME = ClassName.get("android", "R"); /** - * The class name of {@link android.content.res.ResourceId}. - */ - private static final ClassName RESOURCE_ID_CLASS_NAME = ClassName.get( - "android.content.res", "ResourceId"); - - /** * The class name of {@link android.view.inspector.InspectionCompanion}. */ private static final ClassName INSPECTION_COMPANION = ClassName.get( @@ -91,11 +85,11 @@ public final class InspectionCompanionGenerator { private static final String GENERATED_CLASS_SUFFIX = "$$InspectionCompanion"; /** - * The null resource ID. + * The null resource ID, copied to avoid a host dependency on platform code. * * @see android.content.res.Resources#ID_NULL */ - private static final int NO_ID = 0; + private static final int ID_NULL = 0; /** * @param filer A filer to write the generated source to @@ -289,8 +283,8 @@ public final class InspectionCompanionGenerator { if (property.isAttributeIdInferrableFromR()) { builder.add("$T.attr.$L", R_CLASS_NAME, property.getName()); } else { - if (property.getAttributeId() == NO_ID) { - builder.add("$T.ID_NULL", RESOURCE_ID_CLASS_NAME); + if (property.getAttributeId() == ID_NULL) { + builder.add("$L", ID_NULL); } else { builder.add("$L", String.format("0x%08x", property.getAttributeId())); } diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java index 3ffcff8a87d3..6f522608a9b4 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java index e531b67d9ea2..455f5b08e49e 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -46,11 +46,14 @@ import javax.lang.model.element.TypeElement; * @see android.view.inspector.InspectableProperty */ @SupportedAnnotationTypes({ - PlatformInspectableProcessor.NODE_NAME_QUALIFIED_NAME + PlatformInspectableProcessor.NODE_NAME_QUALIFIED_NAME, + PlatformInspectableProcessor.PROPERTY_QUALIFIED_NAME }) public final class PlatformInspectableProcessor extends AbstractProcessor { static final String NODE_NAME_QUALIFIED_NAME = "android.view.inspector.InspectableNodeName"; + static final String PROPERTY_QUALIFIED_NAME = + "android.view.inspector.InspectableProperty"; @Override public SourceVersion getSupportedSourceVersion() { @@ -68,6 +71,11 @@ public final class PlatformInspectableProcessor extends AbstractProcessor { new InspectableNodeNameProcessor(NODE_NAME_QUALIFIED_NAME, processingEnv), modelMap); + } else if (annotation.getQualifiedName().contentEquals(PROPERTY_QUALIFIED_NAME)) { + runModelProcessor( + roundEnv.getElementsAnnotatedWith(annotation), + new InspectablePropertyProcessor(PROPERTY_QUALIFIED_NAME, processingEnv), + modelMap); } else { fail("Unexpected annotation type", annotation); diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java index 6360e0a2de39..b4c6466b2b2f 100644 --- a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java +++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. diff --git a/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor index fa4f71ffd0fa..79185cc80f04 100644 --- a/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor +++ b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor @@ -1 +1 @@ -android.processor.inspector.view.PlatformInspectableProcessor +android.processor.view.inspector.PlatformInspectableProcessor diff --git a/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java index f639719800f6..b0775dc77f45 100644 --- a/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java +++ b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright 2019 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. @@ -20,7 +20,7 @@ import android.processor.view.inspector.InspectableClassModel.Property; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; -import static junit.framework.TestCase.fail; +import static junit.framework.Assert.fail; import com.google.common.base.Charsets; import com.google.common.io.Resources; @@ -65,27 +65,28 @@ public class InspectionCompanionGeneratorTest { @Test public void testSimpleProperties() { - addProperty("boolean", Property.Type.BOOLEAN, "getBoolean"); - addProperty("byte", Property.Type.BYTE, "getByte"); - addProperty("char", Property.Type.CHAR, "getChar"); - addProperty("double", Property.Type.DOUBLE, "getDouble"); - addProperty("float", Property.Type.FLOAT, "getFloat"); - addProperty("int", Property.Type.INT, "getInt"); - addProperty("long", Property.Type.LONG, "getLong"); - addProperty("short", Property.Type.SHORT, "getShort"); - - addProperty("object", Property.Type.OBJECT, "getObject"); - addProperty("color", Property.Type.COLOR, "getColor"); - addProperty("gravity", Property.Type.GRAVITY, "getGravity"); + addProperty("boolean", "getBoolean", Property.Type.BOOLEAN); + addProperty("byte", "getByte", Property.Type.BYTE); + addProperty("char", "getChar", Property.Type.CHAR); + addProperty("double", "getDouble", Property.Type.DOUBLE); + addProperty("float", "getFloat", Property.Type.FLOAT); + addProperty("int", "getInt", Property.Type.INT); + addProperty("long", "getLong", Property.Type.LONG); + addProperty("short", "getShort", Property.Type.SHORT); + + addProperty("object", "getObject", Property.Type.OBJECT); + addProperty("color", "getColor", Property.Type.COLOR); + addProperty("gravity", "getGravity", Property.Type.GRAVITY); assertGeneratedFileEquals("SimpleProperties"); } @Test public void testNoAttributeId() { - final Property property = new Property("noAttributeProperty"); - property.setType(Property.Type.INT); - property.setGetter("getNoAttributeProperty"); + final Property property = new Property( + "noAttributeProperty", + "getNoAttributeProperty", + Property.Type.INT); property.setAttributeIdInferrableFromR(false); mModel.putProperty(property); @@ -94,19 +95,18 @@ public class InspectionCompanionGeneratorTest { @Test public void testSuppliedAttributeId() { - final Property property = new Property("suppliedAttributeProperty"); - property.setType(Property.Type.INT); - property.setGetter("getSuppliedAttributeProperty"); + final Property property = new Property( + "suppliedAttributeProperty", + "getSuppliedAttributeProperty", + Property.Type.INT); property.setAttributeId(0xdecafbad); mModel.putProperty(property); assertGeneratedFileEquals("SuppliedAttributeId"); } - private Property addProperty(String name, Property.Type type, String getter) { - final Property property = new Property(name); - property.setType(type); - property.setGetter(getter); + private Property addProperty(String name, String getter, Property.Type type) { + final Property property = new Property(name, getter, type); mModel.putProperty(property); return property; } diff --git a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NoAttributeId.java.txt b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NoAttributeId.java.txt index 277e84065fb7..23d0f7807aa5 100644 --- a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NoAttributeId.java.txt +++ b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NoAttributeId.java.txt @@ -1,6 +1,5 @@ package com.android.inspectable; -import android.content.res.ResourceId; import android.view.inspector.InspectionCompanion; import android.view.inspector.PropertyMapper; import android.view.inspector.PropertyReader; @@ -25,7 +24,7 @@ public final class TestInspectable$$InspectionCompanion implements InspectionCom @Override public void mapProperties(PropertyMapper propertyMapper) { - mNoAttributePropertyId = propertyMapper.mapInt("noAttributeProperty", ResourceId.ID_NULL); + mNoAttributePropertyId = propertyMapper.mapInt("noAttributeProperty", 0); mPropertiesMapped = true; } |