diff options
author | Phil Weaver <pweaver@google.com> | 2017-11-14 15:20:22 -0800 |
---|---|---|
committer | Phil Weaver <pweaver@google.com> | 2017-11-16 10:22:47 -0800 |
commit | ada70481cb8e184098d9c97e195fcae665818489 (patch) | |
tree | a925c51e13d9d6cb65ff50d7923bcec574032c31 | |
parent | 4cb0d34ba8edb5affdd0bcf76905571b8624c68c (diff) | |
download | experimental-ada70481cb8e184098d9c97e195fcae665818489.tar.gz |
Have TestBack manage accessibility focus.
Test: Build and run TestBack.
Change-Id: I98babde32a483f0ba86c7339bf03b0be71a35644
3 files changed, 196 insertions, 42 deletions
diff --git a/TestBack/src/foo/bar/testback/AccessibilityFocusManager.java b/TestBack/src/foo/bar/testback/AccessibilityFocusManager.java new file mode 100644 index 0000000..c6b91ae --- /dev/null +++ b/TestBack/src/foo/bar/testback/AccessibilityFocusManager.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foo.bar.testback; + +import static foo.bar.testback.AccessibilityNodeInfoUtils.findParent; + +import android.text.TextUtils; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +/** + * Helper class to manage accessibility focus + */ +public class AccessibilityFocusManager { + private static final AccessibilityAction[] IGNORED_ACTIONS_FOR_A11Y_FOCUS = { + AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS, + AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS, + AccessibilityAction.ACTION_SELECT, + AccessibilityAction.ACTION_CLEAR_SELECTION, + AccessibilityAction.ACTION_SHOW_ON_SCREEN + }; + + public static final boolean canTakeAccessibilityFocus(AccessibilityNodeInfo nodeInfo) { + if (nodeInfo.isFocusable() || nodeInfo.isScreenReaderFocusable()) { + return true; + } + + List<AccessibilityAction> actions = new ArrayList<>(nodeInfo.getActionList()); + actions.removeAll(Arrays.asList(IGNORED_ACTIONS_FOR_A11Y_FOCUS)); + + // Nodes with relevant actions are always focusable + if (!actions.isEmpty()) { + return true; + } + + // If a parent is specifically marked focusable, then this node should not be. + if (findParent(nodeInfo, + (parent) -> parent.isFocusable() || parent.isScreenReaderFocusable()) != null) { + return false; + } + + return !TextUtils.isEmpty(nodeInfo.getText()) + || !TextUtils.isEmpty(nodeInfo.getContentDescription()); + }; +} diff --git a/TestBack/src/foo/bar/testback/AccessibilityNodeInfoUtils.java b/TestBack/src/foo/bar/testback/AccessibilityNodeInfoUtils.java new file mode 100644 index 0000000..f4e9398 --- /dev/null +++ b/TestBack/src/foo/bar/testback/AccessibilityNodeInfoUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foo.bar.testback; + +import android.view.accessibility.AccessibilityNodeInfo; + +import java.util.function.Predicate; + +/** + * Utility class for working with AccessibilityNodeInfo + */ +public class AccessibilityNodeInfoUtils { + public static AccessibilityNodeInfo findParent( + AccessibilityNodeInfo start, Predicate<AccessibilityNodeInfo> condition) { + AccessibilityNodeInfo parent = start.getParent(); + if ((parent == null) || (condition.test(parent))) { + return parent; + } + + return findParent(parent, condition); + } + + public static AccessibilityNodeInfo findChildDfs( + AccessibilityNodeInfo start, Predicate<AccessibilityNodeInfo> condition) { + int numChildren = start.getChildCount(); + for (int i = 0; i < numChildren; i++) { + AccessibilityNodeInfo child = start.getChild(i); + if (child != null) { + if (condition.test(child)) { + return child; + } + AccessibilityNodeInfo childResult = findChildDfs(child, condition); + if (childResult != null) { + return childResult; + } + } + } + return null; + } +} diff --git a/TestBack/src/foo/bar/testback/TestBackService.java b/TestBack/src/foo/bar/testback/TestBackService.java index 53db125..bd5329b 100644 --- a/TestBack/src/foo/bar/testback/TestBackService.java +++ b/TestBack/src/foo/bar/testback/TestBackService.java @@ -1,8 +1,30 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package foo.bar.testback; +import static foo.bar.testback.AccessibilityNodeInfoUtils.findChildDfs; +import static foo.bar.testback.AccessibilityNodeInfoUtils.findParent; + +import static foo.bar.testback.AccessibilityFocusManager.CAN_TAKE_A11Y_FOCUS; + import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; +import android.graphics.Rect; import android.util.ArraySet; import android.util.Log; import android.view.WindowManager; @@ -13,6 +35,7 @@ import android.widget.Button; import java.util.List; import java.util.Set; +import java.util.function.Predicate; public class TestBackService extends AccessibilityService { @@ -20,6 +43,11 @@ public class TestBackService extends AccessibilityService { private Button mButton; + int mRowIndexOfA11yFocus = -1; + int mColIndexOfA11yFocus = -1; + AccessibilityNodeInfo mCollectionWithAccessibiltyFocus; + AccessibilityNodeInfo mCurrentA11yFocus; + @Override public void onCreate() { super.onCreate(); @@ -29,53 +57,61 @@ public class TestBackService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { -// if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { -// Log.i(LOG_TAG, event.getText().toString()); -// //dumpWindows(); -// } - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) { -// Log.i(LOG_TAG, "Click event.isChecked()=" + event.isChecked() -// + ((event.getSource() != null) ? " node.isChecked()=" -// + event.getSource().isChecked() : " node=null")); - - AccessibilityNodeInfo source = event.getSource(); - dumpWindow(source); -// AccessibilityNodeInfo node = event.getSource(); -// if (node != null) { -// node.refresh(); -// Log.i(LOG_TAG, "Clicked: " + node.getClassName() + " clicked:" + node.isChecked()); -// } + int eventType = event.getEventType(); + AccessibilityNodeInfo source = event.getSource(); + if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) { + if (source != null) { + AccessibilityNodeInfo focusNode = + (CAN_TAKE_A11Y_FOCUS.test(source)) ? source : findParent( + source, AccessibilityFocusManager::canTakeAccessibilityFocus); + if (focusNode != null) { + focusNode.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + } + return; } - } - - @Override - protected boolean onGesture(int gestureId) { - switch (gestureId) { - case AccessibilityService.GESTURE_SWIPE_DOWN: { - showAccessibilityOverlay(); - } break; - case AccessibilityService.GESTURE_SWIPE_UP: { - hideAccessibilityOverlay(); - } break; + if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + mCurrentA11yFocus = source; + AccessibilityNodeInfo.CollectionItemInfo itemInfo = source.getCollectionItemInfo(); + if (itemInfo == null) { + mRowIndexOfA11yFocus = -1; + mColIndexOfA11yFocus = -1; + mCollectionWithAccessibiltyFocus = null; + } else { + mRowIndexOfA11yFocus = itemInfo.getRowIndex(); + mColIndexOfA11yFocus = itemInfo.getColumnIndex(); + mCollectionWithAccessibiltyFocus = findParent(source, + (nodeInfo) -> nodeInfo.getCollectionInfo() != null); + } + Rect bounds = new Rect(); + source.getBoundsInScreen(bounds); } - return super.onGesture(gestureId); - } - private void showAccessibilityOverlay() { - WindowManager.LayoutParams params = new WindowManager.LayoutParams(); - params.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; - params.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; + if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) { + mCurrentA11yFocus = null; + return; + } - WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - windowManager.addView(mButton, params); - } + if (eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + mRowIndexOfA11yFocus = -1; + mColIndexOfA11yFocus = -1; + mCollectionWithAccessibiltyFocus = null; + } - private void hideAccessibilityOverlay() { - WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - windowManager.removeView(mButton); + if ((mCurrentA11yFocus == null) && (mCollectionWithAccessibiltyFocus != null)) { + // Look for a node to re-focus + AccessibilityNodeInfo focusRestoreTarget = findChildDfs( + mCollectionWithAccessibiltyFocus, (nodeInfo) -> { + AccessibilityNodeInfo.CollectionItemInfo collectionItemInfo = + nodeInfo.getCollectionItemInfo(); + return (collectionItemInfo != null) + && (collectionItemInfo.getRowIndex() == mRowIndexOfA11yFocus) + && (collectionItemInfo.getColumnIndex() == mColIndexOfA11yFocus); + }); + if (focusRestoreTarget != null) { + focusRestoreTarget.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + } } private void dumpWindows() { |