diff --git a/android/plugins/inspector/InspectorSonarPlugin.java b/android/plugins/inspector/InspectorSonarPlugin.java index 1cc4d567e..a7f676e95 100644 --- a/android/plugins/inspector/InspectorSonarPlugin.java +++ b/android/plugins/inspector/InspectorSonarPlugin.java @@ -502,7 +502,7 @@ public class InspectorSonarPlugin implements SonarPlugin { new ErrorReportingRunnable(mConnection) { @Override protected void runOrThrow() throws Exception { - for (int i = 0, count = descriptor.getChildCount(obj); i < count; i++) { + for (int i = 0, count = descriptor.getAXChildCount(obj); i < count; i++) { final Object child = assertNotNull(descriptor.getAXChildAt(obj, i)); children.put(trackObject(child)); } diff --git a/android/plugins/inspector/NodeDescriptor.java b/android/plugins/inspector/NodeDescriptor.java index bb6adac89..c18ad462b 100644 --- a/android/plugins/inspector/NodeDescriptor.java +++ b/android/plugins/inspector/NodeDescriptor.java @@ -100,12 +100,17 @@ public abstract class NodeDescriptor { /** @return The number of children this node exposes in the inspector. */ public abstract int getChildCount(T node) throws Exception; + /** Gets child at index for AX tree. Ignores non-view children. */ + public int getAXChildCount(T node) throws Exception { + return getChildCount(node); + } + /** @return The child at index. */ public abstract Object getChildAt(T node, int index) throws Exception; /** Gets child at index for AX tree. Ignores non-view children. */ public @Nullable Object getAXChildAt(T node, int index) throws Exception { - return null; + return getChildAt(node, index); } /** diff --git a/android/plugins/inspector/descriptors/ViewDescriptor.java b/android/plugins/inspector/descriptors/ViewDescriptor.java index 80760b7ee..1b1041baa 100644 --- a/android/plugins/inspector/descriptors/ViewDescriptor.java +++ b/android/plugins/inspector/descriptors/ViewDescriptor.java @@ -36,6 +36,7 @@ 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.utils.AccessibilityRoleUtil; import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil; import com.facebook.sonar.plugins.inspector.descriptors.utils.EnumMapping; import com.facebook.sonar.plugins.inspector.descriptors.utils.ViewAccessibilityHelper; @@ -43,7 +44,6 @@ import com.facebook.stetho.common.android.ResourcesUtil; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.annotation.Nullable; @@ -102,11 +102,6 @@ public class ViewDescriptor extends NodeDescriptor { return null; } - @Override - public @Nullable Object getAXChildAt(View node, int index) { - return null; - } - @Override public List> getData(View node) { final SonarObject.Builder viewProps = @@ -197,7 +192,9 @@ public class ViewDescriptor extends NodeDescriptor { @Override public List> getAXData(View node) { - return Arrays.asList(new Named<>("AX Properties", getAccessibilityData(node))); + return Arrays.asList( + new Named<>("Derived Props", AccessibilityUtil.getDerivedAXData(node)), + new Named<>("AX Props", AccessibilityUtil.getViewAXData(node))); } private static SonarObject getAccessibilityData(View view) { @@ -225,7 +222,9 @@ public class ViewDescriptor extends NodeDescriptor { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public void setValue(View node, String[] path, SonarDynamic value) { - if (path[0].equals("Accessibility")) { + if (path[0].equals("Accessibility") + || path[0].equals("AX Props") + || path[0].equals("Derived Props")) { setAccessibilityValue(node, path, value); } @@ -433,7 +432,12 @@ public class ViewDescriptor extends NodeDescriptor { } public List> getAXAttributes(View node) throws Exception { - return Collections.EMPTY_LIST; + List> attributes = new ArrayList<>(); + String role = AccessibilityRoleUtil.getRole(node).toString(); + if (!role.equals("NONE")) { + attributes.add(new Named<>("role", role)); + } + return attributes; } @Nullable diff --git a/android/plugins/inspector/descriptors/utils/AccessibilityUtil.java b/android/plugins/inspector/descriptors/utils/AccessibilityUtil.java index 3d556894d..608f0bc73 100644 --- a/android/plugins/inspector/descriptors/utils/AccessibilityUtil.java +++ b/android/plugins/inspector/descriptors/utils/AccessibilityUtil.java @@ -23,6 +23,7 @@ import android.view.accessibility.AccessibilityManager; import android.widget.EditText; import com.facebook.sonar.core.SonarArray; import com.facebook.sonar.core.SonarObject; +import com.facebook.sonar.plugins.inspector.InspectorValue; import javax.annotation.Nullable; /** @@ -354,4 +355,46 @@ public final class AccessibilityUtil { .put("talkback-description", getTalkbackDescription(view)); } } + + public static SonarObject getViewAXData(View view) { + final SonarObject.Builder props = new SonarObject.Builder(); + + // This needs to be an empty string to be mutable. See t20470623. + CharSequence contentDescription = + view.getContentDescription() != null ? view.getContentDescription() : ""; + props.put("content-description", InspectorValue.mutable(contentDescription)); + props.put("focusable", InspectorValue.mutable(view.isFocusable())); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + props.put( + "important-for-accessibility", + AccessibilityUtil.sImportantForAccessibilityMapping.get( + view.getImportantForAccessibility())); + } + + return props.build(); + } + + public static SonarObject getDerivedAXData(View view) { + final SonarObject.Builder props = new SonarObject.Builder(); + + if (!AccessibilityEvaluationUtil.isTalkbackFocusable(view)) { + String reason = getTalkbackIgnoredReasons(view); + props + .put("talkback-ignored", true) + .put("talkback-ignored-reasons", reason == null ? "" : reason); + } else { + String reason = getTalkbackFocusableReasons(view); + CharSequence description = getTalkbackDescription(view); + props + .put("talkback-focusable", true) + .put("talkback-focusable-reasons", reason == null ? "" : reason) + .put("talkback-description", description == null ? "" : description); + } + + SonarObject axProps = getAccessibilityNodeInfoProperties(view); + props.put("node-info", axProps == null ? "null" : axProps); + + return props.build(); + } } diff --git a/android/plugins/inspector/litho-sonar/DebugComponentDescriptor.java b/android/plugins/inspector/litho-sonar/DebugComponentDescriptor.java index f60e422aa..15d4e495f 100644 --- a/android/plugins/inspector/litho-sonar/DebugComponentDescriptor.java +++ b/android/plugins/inspector/litho-sonar/DebugComponentDescriptor.java @@ -12,7 +12,6 @@ import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.v4.util.Pair; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.view.View; import com.facebook.litho.Component; import com.facebook.litho.ComponentContext; @@ -32,7 +31,6 @@ 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.AccessibilityUtil; -import com.facebook.sonar.plugins.inspector.descriptors.utils.ViewAccessibilityHelper; import com.facebook.yoga.YogaAlign; import com.facebook.yoga.YogaDirection; import com.facebook.yoga.YogaEdge; @@ -126,12 +124,12 @@ public class DebugComponentDescriptor extends NodeDescriptor { } @Override - public String getAXName(DebugComponent node) { - View v = node.getComponentHost(); - AccessibilityNodeInfoCompat nodeInfo = ViewAccessibilityHelper.createNodeInfoFromView(v); - String name = nodeInfo.getClassName().toString(); - nodeInfo.recycle(); - return name; + public String getAXName(DebugComponent node) throws Exception { + NodeDescriptor componentDescriptor = descriptorForClass(node.getComponent().getClass()); + if (componentDescriptor.getClass() != ObjectDescriptor.class) { + return componentDescriptor.getAXName(node.getComponent()); + } + return node.getComponent().getSimpleName(); } @Override @@ -143,6 +141,15 @@ public class DebugComponentDescriptor extends NodeDescriptor { } } + @Override + public int getAXChildCount(DebugComponent node) { + if (node.getMountedView() != null) { + return 1; + } else { + return node.getChildComponents().size(); + } + } + @Override public Object getChildAt(DebugComponent node, int index) { final View mountedView = node.getMountedView(); @@ -158,7 +165,7 @@ public class DebugComponentDescriptor extends NodeDescriptor { } @Override - public @Nullable Object getAXChildAt(DebugComponent node, int index) { + public Object getAXChildAt(DebugComponent node, int index) { final View mountedView = node.getMountedView(); if (mountedView != null) { @@ -201,7 +208,11 @@ public class DebugComponentDescriptor extends NodeDescriptor { } @Override - public List> getAXData(DebugComponent node) { + 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> data = new ArrayList<>(); final SonarObject accessibilityData = getAccessibilityData(node); if (accessibilityData != null) {