Manually routing Accessibility clicks for RecyclerView classes.
DocsUI uses TouchDetector to differentiate mouse/gesture events; this
prevents a11y services to know what logic to run when there's a
ACCESSIBILITY_CLICK event. This CL manually adds these accessibility
click events to child views, and also route these to correct click
callbacks.
Test: Manually done
Bug: 32412100
Bug: 30613053
Change-Id: If3bf2a039b3cb269e32555d1740f0420cfa50b93
(cherry picked from commit 62442459cbd6f49c0ae5c1b66dfd925841e20f3d)
diff --git a/src/com/android/documentsui/DropdownBreadcrumb.java b/src/com/android/documentsui/DropdownBreadcrumb.java
index 03b6d09..7f5e5f3 100644
--- a/src/com/android/documentsui/DropdownBreadcrumb.java
+++ b/src/com/android/documentsui/DropdownBreadcrumb.java
@@ -32,7 +32,7 @@
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
-import java.util.function.Consumer;
+import java.util.function.IntConsumer;
/**
* Dropdown implementation of breadcrumb used for phone device layouts
@@ -60,7 +60,7 @@
}
@Override
- public void setup(Environment env, State state, Consumer<Integer> listener) {
+ public void setup(Environment env, State state, IntConsumer listener) {
mAdapter = new DropdownAdapter(state, env);
setOnItemSelectedListener(
new OnItemSelectedListener() {
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index 60d1f96..f45ab67 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -31,8 +31,10 @@
import com.android.documentsui.NavigationViewManager.Environment;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.dirlist.AccessibilityClickEventRouter;
import java.util.function.Consumer;
+import java.util.function.IntConsumer;
/**
* Horizontal implementation of breadcrumb used for tablet / desktop device layouts
@@ -44,7 +46,7 @@
private LinearLayoutManager mLayoutManager;
private BreadcrumbAdapter mAdapter;
- private Consumer<Integer> mListener;
+ private IntConsumer mClickListener;
public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@@ -61,13 +63,20 @@
@Override
public void setup(Environment env,
com.android.documentsui.base.State state,
- Consumer<Integer> listener) {
+ IntConsumer listener) {
- mListener = listener;
+ mClickListener = listener;
mLayoutManager = new LinearLayoutManager(
getContext(), LinearLayoutManager.HORIZONTAL, false);
mAdapter = new BreadcrumbAdapter(
state, env, new ItemDragListener<>(this));
+ // Since we are using GestureDetector to detect click events, a11y services don't know which views
+ // are clickable because we aren't using View.OnClickListener. Thus, we need to use a custom
+ // accessibility delegate to route click events correctly. See AccessibilityClickEventRouter
+ // for more details on how we are routing these a11y events.
+ setAccessibilityDelegateCompat(
+ new AccessibilityClickEventRouter(this,
+ (View child) -> onAccessibilityClick(child)));
setLayoutManager(mLayoutManager);
addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
@@ -108,6 +117,15 @@
return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
}
+ private boolean onAccessibilityClick(View child) {
+ int pos = getChildAdapterPosition(child);
+ if (pos != getAdapter().getItemCount() - 1) {
+ mClickListener.accept(pos);
+ return true;
+ }
+ return false;
+ }
+
@Override
public void postUpdate() {
}
@@ -139,7 +157,7 @@
public void onViewHovered(View v) {
int pos = getChildAdapterPosition(v);
if (pos != mAdapter.getItemCount() - 1) {
- mListener.accept(pos);
+ mClickListener.accept(pos);
}
}
@@ -147,7 +165,7 @@
View itemView = findChildViewUnder(e.getX(), e.getY());
int pos = getChildAdapterPosition(itemView);
if (pos != mAdapter.getItemCount() - 1) {
- mListener.accept(pos);
+ mClickListener.accept(pos);
}
}
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index 07e3824..694381e 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -27,7 +27,7 @@
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.AnimationView;
-import java.util.function.Consumer;
+import java.util.function.IntConsumer;
/**
* A facade over the portions of the app and drawer toolbars.
@@ -124,7 +124,7 @@
}
interface Breadcrumb {
- void setup(Environment env, State state, Consumer<Integer> listener);
+ void setup(Environment env, State state, IntConsumer listener);
void show(boolean visibility);
void postUpdate();
}
diff --git a/src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java b/src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java
new file mode 100644
index 0000000..8cde993
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 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 com.android.documentsui.dirlist;
+
+import android.os.Bundle;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import android.view.View;
+
+import java.util.function.Function;
+
+/**
+ * Custom Accessibility Delegate for RecyclerViews to route click events on its child views to
+ * proper handlers.
+ *
+ * The majority of event handling is done using TouchDetector instead of View.OnCLickListener,
+ * which most a11y services use to understand whether a particular view is clickable or not.
+ * Thus, we need to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child
+ * views' accessibility node, and then correctly route these clicks done by a11y services to responsible
+ * click callbacks.
+ */
+public class AccessibilityClickEventRouter extends RecyclerViewAccessibilityDelegate {
+
+ private final ItemDelegate mItemDelegate;
+ private final Function<View, Boolean> mClickCallback;
+
+ public AccessibilityClickEventRouter(
+ RecyclerView recyclerView, Function<View, Boolean> clickCallback) {
+ super(recyclerView);
+ mClickCallback = clickCallback;
+ mItemDelegate = new ItemDelegate(this) {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(AccessibilityActionCompat.ACTION_CLICK);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ // We are only handling click events; route all other to default implementation
+ if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
+ return mClickCallback.apply(host);
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+ }
+
+ @Override
+ public AccessibilityDelegateCompat getItemDelegate() {
+ return mItemDelegate;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index d032afb..5e27d4e 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -306,6 +306,9 @@
mFocusManager = mInjector.getFocusManager(mRecView, mModel);
mActions = mInjector.getActionHandler(mModel);
+ mRecView.setAccessibilityDelegateCompat(
+ new AccessibilityClickEventRouter(mRecView,
+ (View child) -> onAccessibilityClick(child)));
mSelectionMetadata = new SelectionMetadata(mModel::getItem);
mSelectionMgr.addItemCallback(mSelectionMetadata);
@@ -686,6 +689,12 @@
return false;
}
+ private boolean onAccessibilityClick(View child) {
+ DocumentDetails doc = getDocumentHolder(child);
+ mActions.openDocument(doc);
+ return true;
+ }
+
private void cancelThumbnailTask(View view) {
final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
if (iconThumb != null) {