// Copyright 2004-present Facebook. All Rights Reserved. package com.facebook.litho.sonar; import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO; import static com.facebook.litho.annotations.ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color; import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum; import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Number; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.v4.util.Pair; import android.view.View; import com.facebook.litho.Component; import com.facebook.litho.ComponentContext; import com.facebook.litho.ComponentLifecycle; import com.facebook.litho.DebugComponent; import com.facebook.litho.DebugLayoutNode; import com.facebook.litho.LithoView; import com.facebook.litho.annotations.Prop; import com.facebook.litho.annotations.State; import com.facebook.litho.reference.Reference; import com.facebook.sonar.core.SonarDynamic; import com.facebook.sonar.core.SonarObject; import com.facebook.sonar.plugins.inspector.HighlightedOverlay; import com.facebook.sonar.plugins.inspector.InspectorValue; import com.facebook.sonar.plugins.inspector.Named; import com.facebook.sonar.plugins.inspector.NodeDescriptor; import com.facebook.sonar.plugins.inspector.Touch; import com.facebook.sonar.plugins.inspector.descriptors.ObjectDescriptor; import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityRoleUtil; import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil; import com.facebook.yoga.YogaAlign; import com.facebook.yoga.YogaDirection; import com.facebook.yoga.YogaEdge; import com.facebook.yoga.YogaFlexDirection; import com.facebook.yoga.YogaJustify; import com.facebook.yoga.YogaPositionType; import com.facebook.yoga.YogaValue; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; public class DebugComponentDescriptor extends NodeDescriptor { private static final String axViewPropsTitle = "DebugLayoutNode"; private static final String axNodeInfoPropsTitle = "NodeInfo & TalkBack"; private Map>> mOverrides = new HashMap<>(); private DebugComponent.Overrider mOverrider = new DebugComponent.Overrider() { @Override public void applyComponentOverrides(String key, Component component) { final List> overrides = mOverrides.get(key); if (overrides == null) { return; } for (Pair override : overrides) { if (override.first[0].equals("Props")) { applyReflectiveOverride(component, override.first[1], override.second); } } } @Override public void applyStateOverrides( String key, ComponentLifecycle.StateContainer stateContainer) { final List> overrides = mOverrides.get(key); if (overrides == null) { return; } for (Pair override : overrides) { if (override.first[0].equals("State")) { applyReflectiveOverride(stateContainer, override.first[1], override.second); } } } @Override public void applyLayoutOverrides(String key, DebugLayoutNode node) { final List> overrides = mOverrides.get(key); if (overrides == null) { return; } for (Pair override : overrides) { if (override.first[0].equals("Layout")) { try { applyLayoutOverride( node, Arrays.copyOfRange(override.first, 1, override.first.length), override.second); } catch (Exception ignored) { } } else if (override.first[0].equals("Accessibility") || override.first[0].equals(axViewPropsTitle) || override.first[0].equals(axNodeInfoPropsTitle)) { applyAccessibilityOverride(node, override.first[1], override.second); } } } }; @Override public void init(DebugComponent node) { // We rely on the LithoView being invalidated when a component hierarchy changes. } @Override public String getId(DebugComponent node) { return node.getGlobalKey(); } @Override public String getName(DebugComponent node) throws Exception { NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass()); if (componentDescriptor.getClass() != ObjectDescriptor.class) { return componentDescriptor.getName(node.getComponent()); } return node.getComponent().getSimpleName(); } @Override public int getChildCount(DebugComponent node) { if (node.getMountedView() != null || node.getMountedDrawable() != null) { return 1; } else { return node.getChildComponents().size(); } } @Override public Object getChildAt(DebugComponent node, int index) { final View mountedView = node.getMountedView(); final Drawable mountedDrawable = node.getMountedDrawable(); if (mountedView != null) { return mountedView; } else if (mountedDrawable != null) { return mountedDrawable; } else { return node.getChildComponents().get(index); } } @Override public List> getData(DebugComponent node) throws Exception { NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass()); if (componentDescriptor.getClass() != ObjectDescriptor.class) { return componentDescriptor.getData(node.getComponent()); } final List> data = new ArrayList<>(); final SonarObject layoutData = getLayoutData(node); if (layoutData != null) { data.add(new Named<>("Layout", layoutData)); } final SonarObject propData = getPropData(node); if (propData != null) { data.add(new Named<>("Props", propData)); } final SonarObject stateData = getStateData(node); if (stateData != null) { data.add(new Named<>("State", stateData)); } final SonarObject accessibilityData = getAccessibilityData(node); if (accessibilityData != null) { data.add(new Named<>("Accessibility", accessibilityData)); } return data; } @Override public List> getAXData(DebugComponent node) throws Exception { NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass()); if (componentDescriptor.getClass() != ObjectDescriptor.class) { return componentDescriptor.getAXData(node.getComponent()); } final List> sections = new ArrayList<>(); final SonarObject derivedData = getDerivedAXData(node); if (derivedData != null) { sections.add(new Named<>(axNodeInfoPropsTitle, derivedData)); } final SonarObject viewData = getViewAXData(node); if (viewData != null) { sections.add(new Named<>(axViewPropsTitle, viewData)); } return sections; } @Nullable private static SonarObject getViewAXData(DebugComponent node) { final DebugLayoutNode layout = node.getLayoutNode(); if (layout == null) { return null; } final SonarObject.Builder props = new SonarObject.Builder(); // This needs to be an empty string to be mutable. See t20470623. final CharSequence contentDescription = layout.getContentDescription() != null ? layout.getContentDescription() : ""; props.put("content-description", InspectorValue.mutable(contentDescription)); props.put("focusable", InspectorValue.mutable(layout.getFocusable())); props.put( "important-for-accessibility", AccessibilityUtil.sImportantForAccessibilityMapping.get( layout.getImportantForAccessibility())); return props.build(); } @Nullable private static SonarObject getDerivedAXData(DebugComponent node) { final DebugLayoutNode layout = node.getLayoutNode(); if (layout == null) { return null; } final View hostView = node.getComponentHost(); final SonarObject.Builder props = new SonarObject.Builder(); // No host view exists, so this component is inherently not accessible. Add the reason why this // is the case and then return. if (hostView == node.getLithoView() || hostView == null) { final int importantForAccessibility = layout.getImportantForAccessibility(); final boolean isAccessibilityEnabled = AccessibilityUtil.isAccessibilityEnabled(node.getContext()); String ignoredReason; if (!isAccessibilityEnabled) { ignoredReason = "No accessibility service is running."; } else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO) { ignoredReason = "Component has importantForAccessibility set to NO."; } else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { ignoredReason = "Component has importantForAccessibility set to NO_HIDE_DESCENDANTS."; } else { ignoredReason = "Component does not have content, or accessibility handlers."; } props.put("talkback-ignored", true); props.put("talkback-ignored-reasons", ignoredReason); return props.build(); } // host view exists so add node info and TalkBack properties props.put("node-info", AccessibilityUtil.getAXNodeInfoProperties(hostView)); AccessibilityUtil.addTalkbackProperties(props, hostView); return props.build(); } @Nullable private static SonarObject getLayoutData(DebugComponent node) { final DebugLayoutNode layout = node.getLayoutNode(); if (layout == null) { return null; } final SonarObject.Builder data = new SonarObject.Builder(); data.put("background", fromReference(node.getContext(), layout.getBackground())); data.put("foreground", fromDrawable(layout.getForeground())); data.put("direction", InspectorValue.mutable(Enum, layout.getLayoutDirection().toString())); data.put("flex-direction", InspectorValue.mutable(Enum, layout.getFlexDirection().toString())); data.put( "justify-content", InspectorValue.mutable(Enum, layout.getJustifyContent().toString())); data.put("align-items", InspectorValue.mutable(Enum, layout.getAlignItems().toString())); data.put("align-self", InspectorValue.mutable(Enum, layout.getAlignSelf().toString())); data.put("align-content", InspectorValue.mutable(Enum, layout.getAlignContent().toString())); data.put("position-type", InspectorValue.mutable(Enum, layout.getPositionType().toString())); data.put("flex-grow", fromFloat(layout.getFlexGrow())); data.put("flex-shrink", fromFloat(layout.getFlexShrink())); data.put("flex-basis", fromYogaValue(layout.getFlexBasis())); data.put("width", fromYogaValue(layout.getWidth())); data.put("min-width", fromYogaValue(layout.getMinWidth())); data.put("max-width", fromYogaValue(layout.getMaxWidth())); data.put("height", fromYogaValue(layout.getHeight())); data.put("min-height", fromYogaValue(layout.getMinHeight())); data.put("max-height", fromYogaValue(layout.getMaxHeight())); data.put("aspect-ratio", fromFloat(layout.getAspectRatio())); data.put( "margin", new SonarObject.Builder() .put("left", fromYogaValue(layout.getMargin(YogaEdge.LEFT))) .put("top", fromYogaValue(layout.getMargin(YogaEdge.TOP))) .put("right", fromYogaValue(layout.getMargin(YogaEdge.RIGHT))) .put("bottom", fromYogaValue(layout.getMargin(YogaEdge.BOTTOM))) .put("start", fromYogaValue(layout.getMargin(YogaEdge.START))) .put("end", fromYogaValue(layout.getMargin(YogaEdge.END))) .put("horizontal", fromYogaValue(layout.getMargin(YogaEdge.HORIZONTAL))) .put("vertical", fromYogaValue(layout.getMargin(YogaEdge.VERTICAL))) .put("all", fromYogaValue(layout.getMargin(YogaEdge.ALL)))); data.put( "padding", new SonarObject.Builder() .put("left", fromYogaValue(layout.getPadding(YogaEdge.LEFT))) .put("top", fromYogaValue(layout.getPadding(YogaEdge.TOP))) .put("right", fromYogaValue(layout.getPadding(YogaEdge.RIGHT))) .put("bottom", fromYogaValue(layout.getPadding(YogaEdge.BOTTOM))) .put("start", fromYogaValue(layout.getPadding(YogaEdge.START))) .put("end", fromYogaValue(layout.getPadding(YogaEdge.END))) .put("horizontal", fromYogaValue(layout.getPadding(YogaEdge.HORIZONTAL))) .put("vertical", fromYogaValue(layout.getPadding(YogaEdge.VERTICAL))) .put("all", fromYogaValue(layout.getPadding(YogaEdge.ALL)))); data.put( "border", new SonarObject.Builder() .put("left", fromFloat(layout.getBorderWidth(YogaEdge.LEFT))) .put("top", fromFloat(layout.getBorderWidth(YogaEdge.TOP))) .put("right", fromFloat(layout.getBorderWidth(YogaEdge.RIGHT))) .put("bottom", fromFloat(layout.getBorderWidth(YogaEdge.BOTTOM))) .put("start", fromFloat(layout.getBorderWidth(YogaEdge.START))) .put("end", fromFloat(layout.getBorderWidth(YogaEdge.END))) .put("horizontal", fromFloat(layout.getBorderWidth(YogaEdge.HORIZONTAL))) .put("vertical", fromFloat(layout.getBorderWidth(YogaEdge.VERTICAL))) .put("all", fromFloat(layout.getBorderWidth(YogaEdge.ALL)))); data.put( "position", new SonarObject.Builder() .put("left", fromYogaValue(layout.getPosition(YogaEdge.LEFT))) .put("top", fromYogaValue(layout.getPosition(YogaEdge.TOP))) .put("right", fromYogaValue(layout.getPosition(YogaEdge.RIGHT))) .put("bottom", fromYogaValue(layout.getPosition(YogaEdge.BOTTOM))) .put("start", fromYogaValue(layout.getPosition(YogaEdge.START))) .put("end", fromYogaValue(layout.getPosition(YogaEdge.END))) .put("horizontal", fromYogaValue(layout.getPosition(YogaEdge.HORIZONTAL))) .put("vertical", fromYogaValue(layout.getPosition(YogaEdge.VERTICAL))) .put("all", fromYogaValue(layout.getPosition(YogaEdge.ALL)))); return data.build(); } @Nullable private static SonarObject getPropData(DebugComponent node) { if (node.canResolve()) { return null; } final Component component = node.getComponent(); final SonarObject.Builder props = new SonarObject.Builder(); boolean hasProps = false; for (Field f : component.getClass().getDeclaredFields()) { try { f.setAccessible(true); final Prop annotation = f.getAnnotation(Prop.class); if (annotation != null) { switch (annotation.resType()) { case COLOR: props.put(f.getName(), fromColor((Integer) f.get(component))); break; case DRAWABLE: props.put(f.getName(), fromDrawable((Drawable) f.get(component))); break; default: if (f.get(component) != null && PropWithDescription.class.isAssignableFrom(f.get(component).getClass())) { final Object description = ((PropWithDescription) f.get(component)) .getSonarLayoutInspectorPropDescription(); // Treat the description as immutable for now, because it's a "translation" of the // actual prop, // mutating them is not going to change the original prop. if (description instanceof Map) { final Map descriptionMap = (Map) description; for (Map.Entry entry : descriptionMap.entrySet()) { props.put( entry.getKey().toString(), InspectorValue.immutable(entry.getValue())); } } else { props.put(f.getName(), InspectorValue.immutable(description)); } } else { if (isTypeMutable(f.getType())) { props.put(f.getName(), InspectorValue.mutable(f.get(component))); } else { props.put(f.getName(), InspectorValue.immutable(f.get(component))); } } break; } hasProps = true; } } catch (Exception ignored) { } } return hasProps ? props.build() : null; } @Nullable private static SonarObject getStateData(DebugComponent node) { if (node.canResolve()) { return null; } final ComponentLifecycle.StateContainer stateContainer = node.getStateContainer(); if (stateContainer == null) { return null; } final SonarObject.Builder state = new SonarObject.Builder(); boolean hasState = false; for (Field f : stateContainer.getClass().getDeclaredFields()) { try { f.setAccessible(true); final State annotation = f.getAnnotation(State.class); if (annotation != null) { if (isTypeMutable(f.getType())) { state.put(f.getName(), InspectorValue.mutable(f.get(stateContainer))); } else { state.put(f.getName(), InspectorValue.immutable(f.get(stateContainer))); } hasState = true; } } catch (Exception ignored) { } } return hasState ? state.build() : null; } private static boolean isTypeMutable(Class type) { if (type == int.class || type == Integer.class) { return true; } else if (type == long.class || type == Long.class) { return true; } else if (type == float.class || type == Float.class) { return true; } else if (type == double.class || type == Double.class) { return true; } else if (type == boolean.class || type == Boolean.class) { return true; } else if (type.isAssignableFrom(String.class)) { return true; } return false; } @Nullable private static SonarObject getAccessibilityData(DebugComponent node) { final DebugLayoutNode layout = node.getLayoutNode(); if (layout == null) { return null; } final View hostView = node.getComponentHost(); final SonarObject.Builder accessibilityProps = new SonarObject.Builder(); // This needs to be an empty string to be mutable. See t20470623. final CharSequence contentDescription = layout.getContentDescription() != null ? layout.getContentDescription() : ""; accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription)); accessibilityProps.put("focusable", InspectorValue.mutable(layout.getFocusable())); accessibilityProps.put( "important-for-accessibility", AccessibilityUtil.sImportantForAccessibilityMapping.get( layout.getImportantForAccessibility())); // No host view exists, so this component is inherently not accessible. Add the reason why this // is the case and then return. if (hostView == node.getLithoView() || hostView == null) { final int importantForAccessibility = layout.getImportantForAccessibility(); final boolean isAccessibilityEnabled = AccessibilityUtil.isAccessibilityEnabled(node.getContext()); String ignoredReason; if (!isAccessibilityEnabled) { ignoredReason = "No accessibility service is running."; } else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO) { ignoredReason = "Component has importantForAccessibility set to NO."; } else if (importantForAccessibility == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { ignoredReason = "Component has importantForAccessibility set to NO_HIDE_DESCENDANTS."; } else { ignoredReason = "Component does not have content, or accessibility handlers."; } accessibilityProps.put("talkback-ignored", true); accessibilityProps.put("talkback-ignored-reasons", ignoredReason); return accessibilityProps.build(); } accessibilityProps.put( "node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(hostView)); AccessibilityUtil.addTalkbackProperties(accessibilityProps, hostView); return accessibilityProps.build(); } @Override public void setValue(DebugComponent node, String[] path, SonarDynamic value) { List> overrides = mOverrides.get(node.getGlobalKey()); if (overrides == null) { overrides = new ArrayList<>(); mOverrides.put(node.getGlobalKey(), overrides); } overrides.add(new Pair<>(path, value)); node.setOverrider(mOverrider); node.rerender(); } @Override public List> getAttributes(DebugComponent node) { final List> attributes = new ArrayList<>(); final String key = node.getKey(); final String testKey = node.getTestKey(); if (key != null && key.trim().length() > 0) { attributes.add(new Named<>("key", key)); } if (testKey != null && testKey.trim().length() > 0) { attributes.add(new Named<>("testKey", testKey)); } return attributes; } @Override public List> getAXAttributes(DebugComponent node) { final View hostView = node.getComponentHost(); List> attributes = new ArrayList<>(); String role = AccessibilityRoleUtil.getRole(hostView).toString(); if (!role.equals("NONE")) { attributes.add(new Named<>("role", role)); } return attributes; } @Override public void setHighlighted(DebugComponent node, boolean selected) { final LithoView lithoView = node.getLithoView(); if (lithoView == null) { return; } if (!selected) { HighlightedOverlay.removeHighlight(lithoView); return; } final DebugLayoutNode layout = node.getLayoutNode(); final boolean hasNode = layout != null; final Rect margin; if (!node.isRoot()) { margin = new Rect( hasNode ? (int) layout.getResultMargin(YogaEdge.START) : 0, hasNode ? (int) layout.getResultMargin(YogaEdge.TOP) : 0, hasNode ? (int) layout.getResultMargin(YogaEdge.END) : 0, hasNode ? (int) layout.getResultMargin(YogaEdge.BOTTOM) : 0); } else { // Margin not applied if you're at the root margin = new Rect(); } final Rect padding = new Rect( hasNode ? (int) layout.getResultPadding(YogaEdge.START) : 0, hasNode ? (int) layout.getResultPadding(YogaEdge.TOP) : 0, hasNode ? (int) layout.getResultPadding(YogaEdge.END) : 0, hasNode ? (int) layout.getResultPadding(YogaEdge.BOTTOM) : 0); final Rect contentBounds = node.getBoundsInLithoView(); HighlightedOverlay.setHighlighted(lithoView, margin, padding, contentBounds); } @Override public void hitTest(DebugComponent node, Touch touch) { for (int i = getChildCount(node) - 1; i >= 0; i--) { final Object child = getChildAt(node, i); if (child instanceof DebugComponent) { final DebugComponent componentChild = (DebugComponent) child; final Rect bounds = componentChild.getBounds(); if (touch.containedIn(bounds.left, bounds.top, bounds.right, bounds.bottom)) { touch.continueWithOffset(i, bounds.left, bounds.top); return; } } else if (child instanceof View || child instanceof Drawable) { // Components can only mount one view or drawable and its bounds are the same as the // hosting component. touch.continueWithOffset(i, 0, 0); return; } } touch.finish(); } @Override public String getDecoration(DebugComponent node) throws Exception { if (node.getComponent() != null) { NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass()); if (componentDescriptor.getClass() != ObjectDescriptor.class) { return componentDescriptor.getDecoration(node.getComponent()); } } return "litho"; } @Override public boolean matches(String query, DebugComponent node) throws Exception { NodeDescriptor descriptor = descriptorForClass(Object.class); return descriptor.matches(query, node); } private static void applyAccessibilityOverride( DebugLayoutNode node, String key, SonarDynamic value) { switch (key) { case "focusable": node.setFocusable(value.asBoolean()); break; case "important-for-accessibility": node.setImportantForAccessibility( AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString())); break; case "content-description": node.setContentDescription(value.asString()); break; } } private static void applyLayoutOverride(DebugLayoutNode node, String[] path, SonarDynamic value) { switch (path[0]) { case "background": node.setBackgroundColor(value.asInt()); break; case "foreground": node.setForegroundColor(value.asInt()); break; case "direction": node.setLayoutDirection(YogaDirection.valueOf(value.asString().toUpperCase())); break; case "flex-direction": node.setFlexDirection(YogaFlexDirection.valueOf(value.asString().toUpperCase())); break; case "justify-content": node.setJustifyContent(YogaJustify.valueOf(value.asString().toUpperCase())); break; case "align-items": node.setAlignItems(YogaAlign.valueOf(value.asString().toUpperCase())); break; case "align-self": node.setAlignSelf(YogaAlign.valueOf(value.asString().toUpperCase())); break; case "align-content": node.setAlignContent(YogaAlign.valueOf(value.asString().toUpperCase())); break; case "position-type": node.setPositionType(YogaPositionType.valueOf(value.asString().toUpperCase())); break; case "flex-grow": node.setFlexGrow(value.asFloat()); break; case "flex-shrink": node.setFlexShrink(value.asFloat()); break; case "flex-basis": node.setFlexBasis(YogaValue.parse(value.asString())); break; case "width": node.setWidth(YogaValue.parse(value.asString())); break; case "min-width": node.setMinWidth(YogaValue.parse(value.asString())); break; case "max-width": node.setMaxWidth(YogaValue.parse(value.asString())); break; case "height": node.setHeight(YogaValue.parse(value.asString())); break; case "min-height": node.setMinHeight(YogaValue.parse(value.asString())); break; case "max-height": node.setMaxHeight(YogaValue.parse(value.asString())); break; case "aspect-ratio": node.setAspectRatio(value.asFloat()); break; case "margin": node.setMargin(edgeFromString(path[1]), YogaValue.parse(value.asString())); break; case "padding": node.setPadding(edgeFromString(path[1]), YogaValue.parse(value.asString())); break; case "border": node.setBorderWidth(edgeFromString(path[1]), value.asFloat()); break; case "position": node.setPosition(edgeFromString(path[1]), YogaValue.parse(value.asString())); break; } } private static YogaEdge edgeFromString(String s) { return YogaEdge.valueOf(s.toUpperCase()); } private static void applyReflectiveOverride(Object o, String key, SonarDynamic dynamic) { try { final Field field = o.getClass().getDeclaredField(key); field.setAccessible(true); final Class type = field.getType(); Object value = null; if (type == int.class || type == Integer.class) { value = dynamic.asInt(); } else if (type == long.class || type == Long.class) { value = dynamic.asLong(); } else if (type == float.class || type == Float.class) { value = dynamic.asFloat(); } else if (type == double.class || type == Double.class) { value = dynamic.asDouble(); } else if (type == boolean.class || type == Boolean.class) { value = dynamic.asBoolean(); } else if (type.isAssignableFrom(String.class)) { value = dynamic.asString(); } if (value != null) { field.set(o, value); } } catch (Exception ignored) { } } private static InspectorValue fromDrawable(Drawable d) { if (d instanceof ColorDrawable) { return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor()); } return InspectorValue.mutable(Color, 0); } private static InspectorValue fromReference( ComponentContext c, Reference r) { if (r == null) { return fromDrawable(null); } final T d = Reference.acquire(c, r); final InspectorValue v = fromDrawable(d); Reference.release(c, d, r); return v; } private static InspectorValue fromFloat(float f) { if (Float.isNaN(f)) { return InspectorValue.mutable(Enum, "undefined"); } return InspectorValue.mutable(Number, f); } private static InspectorValue fromYogaValue(YogaValue v) { // TODO add support for Type.Dimension or similar return InspectorValue.mutable(Enum, v.toString()); } private static InspectorValue fromColor(int color) { return InspectorValue.mutable(Color, color); } }