Click to inspect works with TalkBack
Summary: Allow click to inspect to work when Talkback is running (normal tree expands as usual although ax tree doesn't expand yet). Override onHover of Touchoverlay and onRequestSendAccessibilityEvent in viewRoot to prevent talkback focus when in Click to Inspect mode. Also update layout inspector to persist isSearchActive to prevent build up of TouchOverlayViews when navigating between plugins (which happened if leaving the layout inspector while in Click to Inspect mode). Reviewed By: danielbuechele Differential Revision: D9153446 fbshipit-source-id: f76982e8f8cea1e7b7e4c6b9bf73632d101222ef
This commit is contained in:
committed by
Facebook Github Bot
parent
f59cafe25e
commit
fc7f949daf
@@ -15,6 +15,7 @@ import android.view.accessibility.AccessibilityEvent;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
import com.facebook.sonar.core.ErrorReportingRunnable;
|
import com.facebook.sonar.core.ErrorReportingRunnable;
|
||||||
import com.facebook.sonar.core.SonarArray;
|
import com.facebook.sonar.core.SonarArray;
|
||||||
import com.facebook.sonar.core.SonarConnection;
|
import com.facebook.sonar.core.SonarConnection;
|
||||||
@@ -28,6 +29,7 @@ import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
|
|||||||
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
|
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
|
||||||
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
|
||||||
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
|
import com.facebook.sonar.plugins.inspector.descriptors.ApplicationDescriptor;
|
||||||
|
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -136,6 +138,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
connection.receive("setData", mSetData);
|
connection.receive("setData", mSetData);
|
||||||
connection.receive("setHighlighted", mSetHighlighted);
|
connection.receive("setHighlighted", mSetHighlighted);
|
||||||
connection.receive("setSearchActive", mSetSearchActive);
|
connection.receive("setSearchActive", mSetSearchActive);
|
||||||
|
connection.receive("isSearchActive", mIsSearchActive);
|
||||||
connection.receive("getSearchResults", mGetSearchResults);
|
connection.receive("getSearchResults", mGetSearchResults);
|
||||||
connection.receive("getAXRoot", mGetAXRoot);
|
connection.receive("getAXRoot", mGetAXRoot);
|
||||||
connection.receive("getAXNodes", mGetAXNodes);
|
connection.receive("getAXNodes", mGetAXNodes);
|
||||||
@@ -155,7 +158,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
mHighlightedId = null;
|
mHighlightedId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any added accessibility delegates
|
// remove any added accessibility delegates, leave isSearchActive untouched
|
||||||
ApplicationDescriptor.clearEditedDelegates();
|
ApplicationDescriptor.clearEditedDelegates();
|
||||||
|
|
||||||
mObjectTracker.clear();
|
mObjectTracker.clear();
|
||||||
@@ -234,7 +237,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
|
|
||||||
responder.error(
|
responder.error(
|
||||||
new SonarObject.Builder()
|
new SonarObject.Builder()
|
||||||
.put("message", "No node with given id")
|
.put("message", "No accessibility node with given id")
|
||||||
.put("id", id)
|
.put("id", id)
|
||||||
.build());
|
.build());
|
||||||
return;
|
return;
|
||||||
@@ -311,6 +314,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
final boolean active = params.getBoolean("active");
|
final boolean active = params.getBoolean("active");
|
||||||
|
ApplicationDescriptor.setSearchActive(active);
|
||||||
final List<View> roots = mApplication.getViewRoots();
|
final List<View> roots = mApplication.getViewRoots();
|
||||||
|
|
||||||
ViewGroup root = null;
|
ViewGroup root = null;
|
||||||
@@ -334,6 +338,15 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final SonarReceiver mIsSearchActive =
|
||||||
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
|
@Override
|
||||||
|
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
|
||||||
|
throws Exception {
|
||||||
|
responder.success(new SonarObject.Builder().put("isSearchActive", ApplicationDescriptor.getSearchActive()).build());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
final SonarReceiver mGetSearchResults =
|
final SonarReceiver mGetSearchResults =
|
||||||
new MainThreadSonarReceiver(mConnection) {
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
@Override
|
@Override
|
||||||
@@ -354,6 +367,30 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
setBackgroundColor(BoundsDrawable.COLOR_HIGHLIGHT_CONTENT);
|
setBackgroundColor(BoundsDrawable.COLOR_HIGHLIGHT_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onHoverEvent(MotionEvent event) {
|
||||||
|
|
||||||
|
// if in layout inspector and talkback is running, override the first click to locate the clicked view
|
||||||
|
if (mConnection != null && AccessibilityUtil.isTalkbackEnabled(getContext()) && event.getPointerCount() == 1) {
|
||||||
|
final int action = event.getAction();
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_HOVER_ENTER: {
|
||||||
|
event.setAction(MotionEvent.ACTION_DOWN);
|
||||||
|
} break;
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE: {
|
||||||
|
event.setAction(MotionEvent.ACTION_MOVE);
|
||||||
|
} break;
|
||||||
|
case MotionEvent.ACTION_HOVER_EXIT: {
|
||||||
|
event.setAction(MotionEvent.ACTION_UP);
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
return onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise use the default
|
||||||
|
return super.onHoverEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(final MotionEvent event) {
|
public boolean onTouchEvent(final MotionEvent event) {
|
||||||
if (event.getAction() != MotionEvent.ACTION_UP) {
|
if (event.getAction() != MotionEvent.ACTION_UP) {
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static List<ViewGroup> editedDelegates = new ArrayList<>();
|
private static List<ViewGroup> editedDelegates = new ArrayList<>();
|
||||||
|
private static boolean searchActive;
|
||||||
|
|
||||||
|
public static void setSearchActive(boolean active) {
|
||||||
|
searchActive = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getSearchActive() {
|
||||||
|
return searchActive;
|
||||||
|
}
|
||||||
|
|
||||||
private void setDelegates(ApplicationWrapper node) {
|
private void setDelegates(ApplicationWrapper node) {
|
||||||
clearEditedDelegates();
|
clearEditedDelegates();
|
||||||
@@ -70,11 +79,16 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
|||||||
|
|
||||||
// add delegate to root to catch accessibility events so we can update focus in sonar
|
// add delegate to root to catch accessibility events so we can update focus in sonar
|
||||||
view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
|
view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
|
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
|
||||||
if (mConnection != null) {
|
if (mConnection != null) {
|
||||||
|
|
||||||
|
// the touchOverlay will handle the event in this case
|
||||||
|
if (searchActive) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise send the necessary focus event to the plugin
|
||||||
int eventType = event.getEventType();
|
int eventType = event.getEventType();
|
||||||
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
|
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
|
||||||
mConnection.send("axFocusEvent",
|
mConnection.send("axFocusEvent",
|
||||||
@@ -87,6 +101,7 @@ public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
|||||||
.put("isFocus", false)
|
.put("isFocus", false)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return super.onRequestSendAccessibilityEvent(host, child, event);
|
return super.onRequestSendAccessibilityEvent(host, child, event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ public final class AccessibilityUtil {
|
|||||||
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isEnabled();
|
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a {@link Context}, determine if an accessibility touch exploration service (TalkBack) is running.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} used to get the {@link AccessibilityManager}.
|
||||||
|
* @return {@code true} if an accessibility touch exploration service is currently running.
|
||||||
|
*/
|
||||||
|
public static boolean isTalkbackEnabled(Context context) {
|
||||||
|
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isTouchExplorationEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a sentence describing why a given {@link View} will be ignored by Google's TalkBack
|
* Returns a sentence describing why a given {@link View} will be ignored by Google's TalkBack
|
||||||
* screen reader.
|
* screen reader.
|
||||||
|
|||||||
@@ -485,6 +485,12 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
// persist searchActive state when moving between plugins to prevent multiple
|
||||||
|
// TouchOverlayViews since we can't edit the view heirarchy in onDisconnect
|
||||||
|
this.client.call('isSearchActive').then(({isSearchActive}) => {
|
||||||
|
this.dispatchAction({type: 'SetSearchActive', isSearchActive});
|
||||||
|
});
|
||||||
|
|
||||||
performance.mark('LayoutInspectorInitialize');
|
performance.mark('LayoutInspectorInitialize');
|
||||||
this.client.call('getRoot').then((element: Element) => {
|
this.client.call('getRoot').then((element: Element) => {
|
||||||
this.dispatchAction({elements: [element], type: 'UpdateElements'});
|
this.dispatchAction({elements: [element], type: 'UpdateElements'});
|
||||||
@@ -510,9 +516,10 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
this.getNodesAndDirectChildren(path, false).then(
|
this.getNodesAndDirectChildren(path, false).then(
|
||||||
(elements: Array<Element>) => {
|
(elements: Array<Element>) => {
|
||||||
const selected = path[path.length - 1];
|
const selected = path[path.length - 1];
|
||||||
|
const {key, AXkey} = this.getKeysFromSelected(selected);
|
||||||
|
|
||||||
this.dispatchAction({elements, type: 'UpdateElements'});
|
this.dispatchAction({elements, type: 'UpdateElements'});
|
||||||
this.dispatchAction({key: selected, type: 'SelectElement'});
|
this.dispatchAction({key, AXkey, type: 'SelectElement'});
|
||||||
this.dispatchAction({isSearchActive: false, type: 'SetSearchActive'});
|
this.dispatchAction({isSearchActive: false, type: 'SetSearchActive'});
|
||||||
|
|
||||||
for (const key of path) {
|
for (const key of path) {
|
||||||
@@ -719,51 +726,57 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
this.dispatchAction({isAlignmentMode, type: 'SetAlignmentActive'});
|
this.dispatchAction({isAlignmentMode, type: 'SetAlignmentActive'});
|
||||||
};
|
};
|
||||||
|
|
||||||
onElementSelected = debounce((key: ElementID) => {
|
getKeysFromSelected(selectedKey: ElementID) {
|
||||||
let finalKey = key;
|
let key = selectedKey;
|
||||||
let finalAXkey = null;
|
let AXkey = null;
|
||||||
|
|
||||||
if (this.axEnabled()) {
|
if (this.axEnabled()) {
|
||||||
const linkedAXNode =
|
const linkedAXNode =
|
||||||
this.state.elements[key] &&
|
this.state.elements[selectedKey] &&
|
||||||
this.state.elements[key].extraInfo &&
|
this.state.elements[selectedKey].extraInfo &&
|
||||||
this.state.elements[key].extraInfo.linkedAXNode;
|
this.state.elements[selectedKey].extraInfo.linkedAXNode;
|
||||||
|
|
||||||
// element only in main tree with linkedAXNode selected
|
// element only in main tree with linkedAXNode selected
|
||||||
if (linkedAXNode) {
|
if (linkedAXNode) {
|
||||||
finalAXkey = linkedAXNode;
|
AXkey = linkedAXNode;
|
||||||
|
|
||||||
// element only in AX tree with linked nonAX (litho) element selected
|
// element only in AX tree with linked nonAX (litho) element selected
|
||||||
} else if (
|
} else if (
|
||||||
(!this.state.elements[key] ||
|
(!this.state.elements[selectedKey] ||
|
||||||
this.state.elements[key].name === 'ComponentHost') &&
|
this.state.elements[selectedKey].name === 'ComponentHost') &&
|
||||||
this.state.AXtoNonAXMapping[key]
|
this.state.AXtoNonAXMapping[selectedKey]
|
||||||
) {
|
) {
|
||||||
finalKey = this.state.AXtoNonAXMapping[key];
|
key = this.state.AXtoNonAXMapping[selectedKey];
|
||||||
finalAXkey = key;
|
AXkey = selectedKey;
|
||||||
|
|
||||||
// keys are same for both trees or 'linked' element does not exist
|
// keys are same for both trees or 'linked' element does not exist
|
||||||
} else {
|
} else {
|
||||||
finalAXkey = key;
|
AXkey = selectedKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {key, AXkey};
|
||||||
|
}
|
||||||
|
|
||||||
|
onElementSelected = debounce((selectedKey: ElementID) => {
|
||||||
|
const {key, AXkey} = this.getKeysFromSelected(selectedKey);
|
||||||
|
|
||||||
this.dispatchAction({
|
this.dispatchAction({
|
||||||
key: finalKey,
|
key: key,
|
||||||
AXkey: finalAXkey,
|
AXkey: AXkey,
|
||||||
type: 'SelectElement',
|
type: 'SelectElement',
|
||||||
});
|
});
|
||||||
this.client.send('setHighlighted', {
|
this.client.send('setHighlighted', {
|
||||||
id: key,
|
id: selectedKey,
|
||||||
isAlignmentMode: this.state.isAlignmentMode,
|
isAlignmentMode: this.state.isAlignmentMode,
|
||||||
});
|
});
|
||||||
this.getNodes([finalKey], {force: true, ax: false}).then(
|
this.getNodes([key], {force: true, ax: false}).then(
|
||||||
(elements: Array<Element>) => {
|
(elements: Array<Element>) => {
|
||||||
this.dispatchAction({elements, type: 'UpdateElements'});
|
this.dispatchAction({elements, type: 'UpdateElements'});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (this.axEnabled() && finalAXkey) {
|
if (AXkey) {
|
||||||
this.getNodes([finalAXkey], {force: true, ax: true}).then(
|
this.getNodes([AXkey], {force: true, ax: true}).then(
|
||||||
(elements: Array<Element>) => {
|
(elements: Array<Element>) => {
|
||||||
this.dispatchAction({elements, type: 'UpdateAXElements'});
|
this.dispatchAction({elements, type: 'UpdateAXElements'});
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user