1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.dirlist;
18 
19 import android.os.Bundle;
20 import android.view.View;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.core.view.AccessibilityDelegateCompat;
25 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
26 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
27 import androidx.recyclerview.widget.RecyclerView;
28 import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
29 
30 import com.android.documentsui.BreadcrumbHolder;
31 
32 import java.util.function.Function;
33 
34 /**
35  * Custom Accessibility Delegate for RecyclerViews to route click events on its child views to
36  * proper handlers, and to surface selection state to a11y events.
37  * <p>
38  * The majority of event handling isdone using TouchDetector instead of View.OnCLickListener, which
39  * most a11y services use to understand whether a particular view is clickable or not. Thus, we need
40  * to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child views'
41  * accessibility node, and then correctly route these clicks done by a11y services to responsible
42  * click callbacks.
43  * <p>
44  * DocumentsUI uses {@link View#setActivated(boolean)} instead of {@link View#setSelected(boolean)}
45  * for marking a view as selected. We will surface that selection state to a11y services in this
46  * class.
47  */
48 public class AccessibilityEventRouter extends RecyclerViewAccessibilityDelegate {
49 
50     private final ItemDelegate mItemDelegate;
51     private final Function<View, Boolean> mClickCallback;
52     private final Function<View, Boolean> mLongClickCallback;
53 
AccessibilityEventRouter( RecyclerView recyclerView, @NonNull Function<View, Boolean> clickCallback, @Nullable Function<View, Boolean> longClickCallback)54     public AccessibilityEventRouter(
55             RecyclerView recyclerView, @NonNull Function<View, Boolean> clickCallback,
56             @Nullable Function<View, Boolean> longClickCallback) {
57         super(recyclerView);
58         mClickCallback = clickCallback;
59         mLongClickCallback = longClickCallback;
60         mItemDelegate = new ItemDelegate(this) {
61             @Override
62             public void onInitializeAccessibilityNodeInfo(View host,
63                     AccessibilityNodeInfoCompat info) {
64                 super.onInitializeAccessibilityNodeInfo(host, info);
65                 final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(host);
66                 // if the viewHolder is a DocumentsHolder instance and the ItemDetails
67                 // is null, it can't be clicked
68                 if (holder instanceof DocumentHolder) {
69                     if (((DocumentHolder) holder).getItemDetails() != null) {
70                         addAction(info);
71                     }
72                 } else if (holder instanceof BreadcrumbHolder) {
73                     if (!((BreadcrumbHolder) holder).isLast()) {
74                         addAction(info);
75                     }
76                 } else {
77                     addAction(info);
78                 }
79                 info.setSelected(host.isActivated());
80             }
81 
82             @Override
83             public boolean performAccessibilityAction(View host, int action, Bundle args) {
84                 // We are only handling click events; route all other to default implementation
85                 if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
86                     return mClickCallback.apply(host);
87                 } else if (action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK
88                         && mLongClickCallback != null) {
89                     return mLongClickCallback.apply(host);
90                 }
91                 return super.performAccessibilityAction(host, action, args);
92             }
93         };
94     }
95 
96     @Override
getItemDelegate()97     public AccessibilityDelegateCompat getItemDelegate() {
98         return mItemDelegate;
99     }
100 
addAction(AccessibilityNodeInfoCompat info)101     private void addAction(AccessibilityNodeInfoCompat info) {
102         info.addAction(AccessibilityActionCompat.ACTION_CLICK);
103         if (mLongClickCallback != null) {
104             info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
105         }
106     }
107 }