Allow for multiple view roots, include accessibility focus changing between view roots

Summary: Ax mode now works with multiple view roots/windows, accessibility focus is also updated when new windows are opened.

Reviewed By: danielbuechele

Differential Revision: D9121844

fbshipit-source-id: 1da9327f5d6a784793db8076c2ad2d84e860ac1c
This commit is contained in:
Sara Valderrama
2018-08-02 09:28:22 -07:00
committed by Facebook Github Bot
parent 0b0f59f096
commit ff0b045bde
7 changed files with 164 additions and 70 deletions

View File

@@ -27,6 +27,8 @@ import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
@@ -153,6 +155,9 @@ public class InspectorSonarPlugin implements SonarPlugin {
mHighlightedId = null;
}
// remove any added accessibility delegates
ApplicationDescriptor.clearEditedDelegates();
mObjectTracker.clear();
mDescriptorMapping.onDisconnect();
mConnection = null;
@@ -172,41 +177,8 @@ public class InspectorSonarPlugin implements SonarPlugin {
@Override
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
throws Exception {
final List<View> viewRoots = mApplication.getViewRoots();
ViewGroup root = null;
for (int i = viewRoots.size() - 1; i >= 0; i--) {
if (viewRoots.get(i) instanceof ViewGroup) {
root = (ViewGroup) viewRoots.get(i);
break;
}
}
if (root != null) {
// unlikely, but check to make sure accessibility functionality doesn't change
if (!ViewCompat.hasAccessibilityDelegate(root)) {
// add delegate to root to catch accessibility events so we can update focus in sonar
root.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED || eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) {
mConnection.send("axFocusEvent",
new SonarObject.Builder()
.put("isFocus", eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
.build());
}
return super.onRequestSendAccessibilityEvent(host, child, event);
}
});
}
responder.success(getAXNode(trackObject(root)));
}
// applicationWrapper is not used by accessibility, but is a common ancestor for multiple view roots
responder.success(getAXNode(trackObject(mApplication)));
}
};
@@ -245,21 +217,42 @@ public class InspectorSonarPlugin implements SonarPlugin {
final SonarArray ids = params.getArray("ids");
final SonarArray.Builder result = new SonarArray.Builder();
// getNodes called to refresh accessibility focus
final boolean forFocusEvent = params.getBoolean("forFocusEvent");
for (int i = 0, count = ids.length(); i < count; i++) {
final String id = ids.getString(i);
final SonarObject node = getAXNode(id);
if (node != null) {
result.put(node);
} else {
// sent request for non-existent node, potentially in error
if (node == null) {
// some nodes may be null since we are searching through all current and previous known nodes
if (forFocusEvent) {
continue;
}
responder.error(
new SonarObject.Builder()
.put("message", "No node with given id")
.put("id", id)
.build());
new SonarObject.Builder()
.put("message", "No node with given id")
.put("id", id)
.build());
return;
} else {
// only need to get the focused node in this case
if (forFocusEvent) {
if (node.getObject("extraInfo").getBoolean("focused")) {
result.put(node);
break;
}
// normal getNodes call, put any nodes in result
} else {
result.put(node);
}
}
}
responder.success(new SonarObject.Builder().put("elements", result).build());
}
};

View File

@@ -22,7 +22,7 @@ import javax.annotation.Nullable;
* data can be exposed to the inspector.
*/
public abstract class NodeDescriptor<T> {
private SonarConnection mConnection;
protected SonarConnection mConnection;
private DescriptorMapping mDescriptorMapping;
void setConnection(SonarConnection connection) {
@@ -63,6 +63,26 @@ public abstract class NodeDescriptor<T> {
}
}
/**
* Invalidate a node in the ax tree. This tells Sonar that this node is no longer valid and its properties and/or
* children have changed. This will trigger Sonar to re-query this node getting any new data.
*/
protected final void invalidateAX(final T node) {
if (mConnection != null) {
new ErrorReportingRunnable() {
@Override
protected void runOrThrow() throws Exception {
SonarArray array =
new SonarArray.Builder()
.put(new SonarObject.Builder().put("id", getId(node)).build())
.build();
SonarObject params = new SonarObject.Builder().put("nodes", array).build();
mConnection.send("invalidateAX", params);
}
}.run();
}
}
protected final boolean connected() {
return mConnection != null;
}

View File

@@ -9,14 +9,18 @@
package com.facebook.sonar.plugins.inspector.descriptors;
import android.app.Activity;
import android.support.v4.view.ViewCompat;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import com.facebook.sonar.core.SonarDynamic;
import com.facebook.sonar.core.SonarObject;
import com.facebook.sonar.plugins.inspector.ApplicationWrapper;
import com.facebook.sonar.plugins.inspector.Named;
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
import com.facebook.sonar.plugins.inspector.Touch;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
@@ -55,6 +59,50 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
}
}
private static List<ViewGroup> editedDelegates = new ArrayList<>();
private void setDelegates(ApplicationWrapper node) {
clearEditedDelegates();
for (View view : node.getViewRoots()) {
// unlikely, but check to make sure accessibility functionality doesn't change
if (view instanceof ViewGroup && !ViewCompat.hasAccessibilityDelegate(view)) {
// add delegate to root to catch accessibility events so we can update focus in sonar
view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
if (mConnection != null) {
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
mConnection.send("axFocusEvent",
new SonarObject.Builder()
.put("isFocus", true)
.build());
} else if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) {
mConnection.send("axFocusEvent",
new SonarObject.Builder()
.put("isFocus", false)
.build());
}
}
return super.onRequestSendAccessibilityEvent(host, child, event);
}
});
editedDelegates.add((ViewGroup) view);
}
}
}
public static void clearEditedDelegates() {
for (ViewGroup viewGroup : editedDelegates) {
viewGroup.setAccessibilityDelegate(null);
}
editedDelegates.clear();
}
@Override
public void init(final ApplicationWrapper node) {
node.setListener(
@@ -62,6 +110,8 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
@Override
public void onActivityStackChanged(List<Activity> stack) {
invalidate(node);
invalidateAX(node);
setDelegates(node);
}
});
@@ -73,6 +123,8 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
if (connected()) {
if (key.set(node)) {
invalidate(node);
invalidateAX(node);
setDelegates(node);
}
node.postDelayed(this, 1000);
}
@@ -92,6 +144,11 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
return node.getApplication().getPackageName();
}
@Override
public String getAXName(ApplicationWrapper node) throws Exception {
return "Application";
}
@Override
public int getChildCount(ApplicationWrapper node) {
return node.getViewRoots().size();
@@ -110,6 +167,11 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
return view;
}
@Override
public Object getAXChildAt(ApplicationWrapper node, int index) {
return node.getViewRoots().get(index);
}
@Override
public List<Named<SonarObject>> getData(ApplicationWrapper node) {
return Collections.EMPTY_LIST;

View File

@@ -19,6 +19,7 @@ import android.os.Build;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
@@ -85,14 +86,19 @@ public class ViewDescriptor extends NodeDescriptor<View> {
}
@Override
public String getAXName(View node) {
public String getAXName(View node) throws Exception {
AccessibilityNodeInfoCompat nodeInfo = ViewAccessibilityHelper.createNodeInfoFromView(node);
if (nodeInfo == null) {
return "NULL NODEINFO";
if (nodeInfo != null) {
CharSequence name = nodeInfo.getClassName();
nodeInfo.recycle();
if (name != null) {
return name.toString();
}
}
String name = nodeInfo.getClassName().toString();
nodeInfo.recycle();
return name;
return "NULL NODEINFO OR CLASSNAME";
}
@Override
@@ -418,7 +424,7 @@ public class ViewDescriptor extends NodeDescriptor<View> {
node.setSelected(value.asBoolean());
break;
}
invalidate(node);
invalidateAX(node);
}
@Override

View File

@@ -73,6 +73,7 @@ public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
if (connected()) {
if (key.set(node)) {
invalidate(node);
invalidateAX(node);
}
final boolean hasAttachedToWindow =

View File

@@ -24,6 +24,7 @@ public class LithoViewDescriptor extends NodeDescriptor<LithoView> {
@Override
public void onDirtyMount(LithoView view) {
invalidate(view);
invalidateAX(view);
}
});
}