1 /*
2  * Copyright (C) 2020 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.systemui.qs.customize;
18 
19 import android.os.Bundle;
20 import android.view.View;
21 import android.view.accessibility.AccessibilityNodeInfo;
22 
23 import androidx.core.view.AccessibilityDelegateCompat;
24 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
25 
26 import com.android.systemui.res.R;
27 
28 import java.util.List;
29 
30 /**
31  * Accessibility delegate for {@link TileAdapter} views.
32  *
33  * This delegate will populate the accessibility info with the proper actions that can be taken for
34  * the different tiles:
35  * <ul>
36  *   <li>Add to end if the tile is not a current tile (by double tap).</li>
37  *   <li>Add to a given position (by context menu). This will let the user select a position.</li>
38  *   <li>Remove, if the tile is a current tile (by double tap).</li>
39  *   <li>Move to a given position (by context menu). This will let the user select a position.</li>
40  * </ul>
41  *
42  * This only handles generating the associated actions. The logic for selecting positions is handled
43  * by {@link TileAdapter}.
44  *
45  * In order for the delegate to work properly, the asociated {@link TileAdapter.Holder} should be
46  * passed along with the view using {@link View#setTag}.
47  */
48 class TileAdapterDelegate extends AccessibilityDelegateCompat {
49 
50     private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position;
51     private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position;
52 
getHolder(View view)53     private TileAdapter.Holder getHolder(View view) {
54         return (TileAdapter.Holder) view.getTag();
55     }
56 
57     @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)58     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
59         super.onInitializeAccessibilityNodeInfo(host, info);
60         TileAdapter.Holder holder = getHolder(host);
61         info.setCollectionItemInfo(null);
62         info.setStateDescription("");
63         if (holder == null || !holder.canTakeAccessibleAction()) {
64             // If there's not a holder (not a regular Tile) or an action cannot be taken
65             // because we are in the middle of an accessibility action, don't create a special node.
66             return;
67         }
68 
69         addClickAction(host, info, holder);
70         maybeAddActionAddToPosition(host, info, holder);
71         maybeAddActionMoveToPosition(host, info, holder);
72 
73         if (holder.isCurrentTile()) {
74             info.setStateDescription(host.getContext().getString(
75                     R.string.accessibility_qs_edit_position, holder.getLayoutPosition()));
76         }
77     }
78 
79     @Override
performAccessibilityAction(View host, int action, Bundle args)80     public boolean performAccessibilityAction(View host, int action, Bundle args) {
81         TileAdapter.Holder holder = getHolder(host);
82 
83         if (holder == null || !holder.canTakeAccessibleAction()) {
84             // If there's not a holder (not a regular Tile) or an action cannot be taken
85             // because we are in the middle of an accessibility action, perform the default action.
86             return super.performAccessibilityAction(host, action, args);
87         }
88         if (action == AccessibilityNodeInfo.ACTION_CLICK) {
89             holder.toggleState();
90             return true;
91         } else if (action == MOVE_TO_POSITION_ID) {
92             holder.startAccessibleMove();
93             return true;
94         } else if (action == ADD_TO_POSITION_ID) {
95             holder.startAccessibleAdd();
96             return true;
97         } else {
98             return super.performAccessibilityAction(host, action, args);
99         }
100     }
101 
addClickAction( View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder)102     private void addClickAction(
103             View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
104         String clickActionString;
105         if (holder.canAdd()) {
106             clickActionString = host.getContext().getString(
107                     R.string.accessibility_qs_edit_tile_add_action);
108         } else if (holder.canRemove()) {
109             clickActionString = host.getContext().getString(
110                     R.string.accessibility_qs_edit_remove_tile_action);
111         } else {
112             // Remove the default click action if tile can't either be added or removed (for example
113             // if there's the minimum number of tiles)
114             List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> listOfActions =
115                     info.getActionList(); // This is a copy
116             int numActions = listOfActions.size();
117             for (int i = 0; i < numActions; i++) {
118                 if (listOfActions.get(i).getId() == AccessibilityNodeInfo.ACTION_CLICK) {
119                     info.removeAction(listOfActions.get(i));
120                 }
121             }
122             // We really don't want it to be clickable in this case.
123             info.setClickable(false);
124             return;
125         }
126 
127         AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
128                 new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
129                         AccessibilityNodeInfo.ACTION_CLICK, clickActionString);
130         info.addAction(action);
131         info.setClickable(true);
132     }
133 
maybeAddActionMoveToPosition( View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder)134     private void maybeAddActionMoveToPosition(
135             View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
136         if (holder.isCurrentTile()) {
137             AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
138                     new AccessibilityNodeInfoCompat.AccessibilityActionCompat(MOVE_TO_POSITION_ID,
139                             host.getContext().getString(
140                                     R.string.accessibility_qs_edit_tile_start_move));
141             info.addAction(action);
142         }
143     }
144 
maybeAddActionAddToPosition( View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder)145     private void maybeAddActionAddToPosition(
146             View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
147         if (holder.canAdd()) {
148             AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
149                     new AccessibilityNodeInfoCompat.AccessibilityActionCompat(ADD_TO_POSITION_ID,
150                             host.getContext().getString(
151                                     R.string.accessibility_qs_edit_tile_start_add));
152             info.addAction(action);
153         }
154     }
155 }
156