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) {