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:
committed by
Facebook Github Bot
parent
0b0f59f096
commit
ff0b045bde
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,6 +73,7 @@ public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
|
||||
if (connected()) {
|
||||
if (key.set(node)) {
|
||||
invalidate(node);
|
||||
invalidateAX(node);
|
||||
}
|
||||
|
||||
final boolean hasAttachedToWindow =
|
||||
|
||||
@@ -24,6 +24,7 @@ public class LithoViewDescriptor extends NodeDescriptor<LithoView> {
|
||||
@Override
|
||||
public void onDirtyMount(LithoView view) {
|
||||
invalidate(view);
|
||||
invalidateAX(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user