aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Weaver <pweaver@google.com>2017-11-14 15:20:22 -0800
committerPhil Weaver <pweaver@google.com>2017-11-16 10:22:47 -0800
commitada70481cb8e184098d9c97e195fcae665818489 (patch)
treea925c51e13d9d6cb65ff50d7923bcec574032c31
parent4cb0d34ba8edb5affdd0bcf76905571b8624c68c (diff)
downloadexperimental-ada70481cb8e184098d9c97e195fcae665818489.tar.gz
Have TestBack manage accessibility focus.
Test: Build and run TestBack. Change-Id: I98babde32a483f0ba86c7339bf03b0be71a35644
-rw-r--r--TestBack/src/foo/bar/testback/AccessibilityFocusManager.java64
-rw-r--r--TestBack/src/foo/bar/testback/AccessibilityNodeInfoUtils.java54
-rw-r--r--TestBack/src/foo/bar/testback/TestBackService.java120
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() {