Add InspectionHelper and related interfaces
Create a new package, android.view.inspector, which contains the
InspectionHelper interface, and the associated interfaces
ChildTraverser, PropertyMapper, and PropertyReader.
Test: m
Bug: 118895011
Change-Id: Iaedf1c82ac302bbc467300065b8e1612baf57ad7
diff --git a/core/java/android/view/inspector/ChildTraverser.java b/core/java/android/view/inspector/ChildTraverser.java
new file mode 100644
index 0000000..b775de5
--- /dev/null
+++ b/core/java/android/view/inspector/ChildTraverser.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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.view.inspector;
+
+import android.annotation.NonNull;
+
+/**
+ * Interface for visiting all the child nodes of an inspectable object.
+ *
+ * Inspectable objects may return a collection of children as an array, an {@link Iterable} or an
+ * {@link java.util.Iterator}. This provides a unified API for traversing across all the children
+ * of an inspectable node.
+ *
+ * This interface is consumed by {@link InspectionHelper#traverseChildren(Object, ChildTraverser)}
+ * and may be implemented as a lambda.
+ *
+ * @see InspectionHelper#traverseChildren(Object, ChildTraverser)
+ * @hide
+ */
+@FunctionalInterface
+public interface ChildTraverser {
+ /**
+ * Visit one child object of a parent inspectable object.
+ *
+ * The iteration interface will filter null values out before passing them to this method, but
+ * some child objects may not be inspectable. It is up to the implementor to determine their
+ * inspectablity and what to do with them.
+ *
+ * @param child A child object, guaranteed not to be null.
+ */
+ void traverseChild(@NonNull Object child);
+}
diff --git a/core/java/android/view/inspector/InspectionHelper.java b/core/java/android/view/inspector/InspectionHelper.java
new file mode 100644
index 0000000..27a9704
--- /dev/null
+++ b/core/java/android/view/inspector/InspectionHelper.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 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.view.inspector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * An interface for companion objects used to inspect views.
+ *
+ * Inspection helpers only need to handle the properties, name and traversal of the specific class
+ * they are defined for, not anything from a parent class. At runtime, the inspector instantiates
+ * one instance of each inspection helper, and handles visiting them in the correct inheritance
+ * order for each type it inspects.
+ *
+ * Properties are read from the top of the type tree to the bottom, so that classes that override
+ * a property in their parent class can overwrite it in the reader. In general, properties will
+ * cleanly inherit through their getters, and the inspector runtime will read the properties of a
+ * parent class via the parent's inspection helper, and the child helper will only read properties
+ * added or changed since the parent was defined.
+ *
+ * Only one child traversal is considered for each class. If a descendant class defines a
+ * different child traversal than its parent, only the bottom traversal is used. If a class does
+ * not define its own child traversal, but one of its ancestors does, the bottom-most ancestor's
+ * traversal will be used.
+ *
+ * @param <T> The type of inspectable this helper operates on
+ * @hide
+ */
+public interface InspectionHelper<T> {
+ /**
+ * Map the string names of the properties this helper knows about to integer IDs.
+ *
+ * Each helper is responsible for storing the integer IDs of all its properties. This is the
+ * only method that is allowed to modify the stored IDs.
+ *
+ * Calling {@link #readProperties(T, PropertyReader)} before calling this results in
+ * undefined behavior.
+ *
+ * @param propertyMapper A {@link PropertyMapper} or lambda which maps string names to IDs.
+ */
+ void mapProperties(@NonNull PropertyMapper propertyMapper);
+
+ /**
+ * Read the values of an instance of this helper's type into a {@link PropertyReader}.
+ *
+ * This method needs to return the property IDs stored by
+ * {@link #mapProperties(PropertyMapper)}. Implementations should track if their properties
+ * have been mapped and throw a {@link UninitializedPropertyMapException} if this method is
+ * called before {mapProperties}.
+ *
+ * @param inspectable A object of type {@link T} to read the properties of.
+ * @param propertyReader An object which receives the property IDs and values.
+ */
+ void readProperties(@NonNull T inspectable, @NonNull PropertyReader propertyReader);
+
+ /**
+ * Query if this inspectable type can potentially have child nodes.
+ *
+ * E.g.: any descendant of {@link android.view.ViewGroup} can have child nodes, but a leaf
+ * view like {@link android.widget.ImageView} may not.
+ *
+ * The default implementation always returns false. If an implementing class overrides this, it
+ * should also define {@link #traverseChildren(T, ChildTraverser)}.
+ *
+ * @return True if this inspectable type can potentially have child nodes, false otherwise.
+ */
+ default boolean hasChildTraversal() {
+ return false;
+ }
+
+ /**
+ * Traverse the child nodes of an instance of this helper's type into a {@link ChildTraverser}.
+ *
+ * This provides the ability to traverse over a variety of collection APIs (e.g.: arrays,
+ * {@link Iterable}, or {@link java.util.Iterator}) in a uniform fashion. The traversal must be
+ * in the order defined by this helper's type. If the getter returns null, the helper must
+ * treat it as an empty collection.
+ *
+ * The default implementation throws a {@link NoChildTraversalException}. If
+ * {@link #hasChildTraversal()} returns is overriden to return true, it is expected that the
+ * implementing class will also override this method and provide a traversal.
+ *
+ * @param inspectable An object of type {@link T} to traverse the child nodes of.
+ * @param childTraverser A {@link ChildTraverser} or lamba to receive the children in order.
+ * @throws NoChildTraversalException If there is no defined child traversal
+ */
+ default void traverseChildren(
+ @NonNull T inspectable,
+ @SuppressWarnings("unused") @NonNull ChildTraverser childTraverser) {
+ throw new NoChildTraversalException(inspectable.getClass());
+ }
+
+ /**
+ * Get an optional name to display to developers for inspection nodes of this helper's type.
+ *
+ * The default implementation returns null, which will cause the runtime to use the class's
+ * simple name as defined by {@link Class#getSimpleName()} as the node name.
+ *
+ * If the type of this helper is inflated from XML, this method should be overridden to return
+ * the string used as the tag name for this type in XML.
+ *
+ * @return A string to use as the node name, or null to use the simple class name fallback.
+ */
+ @Nullable
+ default String getNodeName() {
+ return null;
+ }
+
+ /**
+ * Thrown by {@link #readProperties(Object, PropertyReader)} if called before
+ * {@link #mapProperties(PropertyMapper)}.
+ */
+ class UninitializedPropertyMapException extends RuntimeException {
+ public UninitializedPropertyMapException() {
+ super("Unable to read properties of an inspectable before mapping their IDs.");
+ }
+ }
+
+ /**
+ * Thrown by {@link #traverseChildren(Object, ChildTraverser)} if no child traversal exists.
+ */
+ class NoChildTraversalException extends RuntimeException {
+ public NoChildTraversalException(Class cls) {
+ super(String.format(
+ "Class %s does not have a defined child traversal. Cannot traverse children.",
+ cls.getCanonicalName()
+ ));
+ }
+ }
+}
diff --git a/core/java/android/view/inspector/OWNERS b/core/java/android/view/inspector/OWNERS
new file mode 100644
index 0000000..0473f54
--- /dev/null
+++ b/core/java/android/view/inspector/OWNERS
@@ -0,0 +1,3 @@
+alanv@google.com
+ashleyrose@google.com
+aurimas@google.com
\ No newline at end of file
diff --git a/core/java/android/view/inspector/PropertyMapper.java b/core/java/android/view/inspector/PropertyMapper.java
new file mode 100644
index 0000000..35550bd
--- /dev/null
+++ b/core/java/android/view/inspector/PropertyMapper.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 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.view.inspector;
+
+import android.annotation.NonNull;
+
+/**
+ * An interface for mapping the string names of inspectable properties to integer identifiers.
+ *
+ * This interface is consumed by {@link InspectionHelper#mapProperties(PropertyMapper)}.
+ *
+ * Mapping properties to IDs enables quick comparisons against shadow copies of inspectable
+ * objects without performing a large number of string comparisons.
+ *
+ * @see InspectionHelper#mapProperties(PropertyMapper)
+ * @hide
+ */
+public interface PropertyMapper {
+ /**
+ * Map a string name to an integer ID for a primitive boolean property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapBoolean(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive byte property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapByte(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive char property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapChar(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive double property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapDouble(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive float property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapFloat(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive int property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapInt(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive long property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapLong(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for a primitive short property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapShort(@NonNull String name);
+
+ /**
+ * Map a string name to an integer ID for an object property.
+ *
+ * @param name The name of the property
+ * @return An integer ID for the property
+ * @throws PropertyConflictException If the property name is already mapped as another type.
+ */
+ int mapObject(@NonNull String name);
+
+ /**
+ * Thrown from a map method if a property name is already mapped as different type.
+ */
+ class PropertyConflictException extends RuntimeException {
+ public PropertyConflictException(
+ @NonNull String name,
+ @NonNull String newPropertyType,
+ @NonNull String existingPropertyType) {
+ super(String.format(
+ "Attempted to map property \"%s\" as type %s, but it is already mapped as %s.",
+ name,
+ newPropertyType,
+ existingPropertyType
+ ));
+ }
+ }
+}
diff --git a/core/java/android/view/inspector/PropertyReader.java b/core/java/android/view/inspector/PropertyReader.java
new file mode 100644
index 0000000..df81c10
--- /dev/null
+++ b/core/java/android/view/inspector/PropertyReader.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 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.view.inspector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * An interface for reading the properties of an inspectable object.
+ *
+ * Used as the parameter for {@link InspectionHelper#readProperties(Object, PropertyReader)}.
+ * It has separate methods for all primitive types to avoid autoboxing overhead if a concrete
+ * implementation is able to work with primitives. Implementations should be prepared to accept
+ * {null} as the value of {@link PropertyReader#readObject(int, Object)}.
+ *
+ * @see InspectionHelper#readProperties(Object, PropertyReader)
+ * @hide
+ */
+public interface PropertyReader {
+ /**
+ * Read a primitive boolean property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {boolean}
+ */
+ void readBoolean(int id, boolean value);
+
+ /**
+ * Read a primitive byte property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {byte}
+ */
+ void readByte(int id, byte value);
+
+ /**
+ * Read a primitive character property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {char}
+ */
+ void readChar(int id, char value);
+
+ /**
+ * Read a read a primitive double property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {double}
+ */
+ void readDouble(int id, double value);
+
+ /**
+ * Read a primitive float property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {float}
+ */
+ void readFloat(int id, float value);
+
+ /**
+ * Read a primitive integer property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an {int}
+ */
+ void readInt(int id, int value);
+
+ /**
+ * Read a primitive long property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {long}
+ */
+ void readLong(int id, long value);
+
+ /**
+ * Read a primitive short property.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as a {short}
+ */
+ void readShort(int id, short value);
+
+ /**
+ * Read any object as a property.
+ *
+ * If value is null, the property is marked as empty.
+ *
+ * @param id Identifier of the property from a {@link PropertyMapper}
+ * @param value Value of the property
+ * @throws PropertyTypeMismatchException If the property ID is not mapped as an object
+ */
+ void readObject(int id, @Nullable Object value);
+
+ /**
+ * Thrown if a client calls a typed read method for a property of a different type.
+ */
+ class PropertyTypeMismatchException extends RuntimeException {
+ public PropertyTypeMismatchException(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType,
+ @Nullable String propertyName) {
+ super(formatMessage(id, expectedPropertyType, actualPropertyType, propertyName));
+ }
+
+ public PropertyTypeMismatchException(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType) {
+ super(formatMessage(id, expectedPropertyType, actualPropertyType, null));
+ }
+
+ private static @NonNull String formatMessage(
+ int id,
+ @NonNull String expectedPropertyType,
+ @NonNull String actualPropertyType,
+ @Nullable String propertyName) {
+
+ if (propertyName == null) {
+ return String.format(
+ "Attempted to read property with ID 0x%08X as type %s, "
+ + "but the ID is of type %s.",
+ id,
+ expectedPropertyType,
+ actualPropertyType
+ );
+ } else {
+ return String.format(
+ "Attempted to read property \"%s\" with ID 0x%08X as type %s, "
+ + "but the ID is of type %s.",
+ propertyName,
+ id,
+ expectedPropertyType,
+ actualPropertyType
+ );
+ }
+ }
+ }
+}