1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.accessibility;
17 
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.os.Bundle;
21 import android.text.TextUtils;
22 import android.util.SparseArray;
23 import android.view.View;
24 import android.view.accessibility.AccessibilityNodeInfo;
25 
26 import com.android.launcher3.BubbleTextView;
27 import com.android.launcher3.DropTarget;
28 import com.android.launcher3.LauncherSettings;
29 import com.android.launcher3.dragndrop.DragController;
30 import com.android.launcher3.dragndrop.DragOptions;
31 import com.android.launcher3.model.data.CollectionInfo;
32 import com.android.launcher3.model.data.ItemInfo;
33 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
34 import com.android.launcher3.model.data.WorkspaceItemInfo;
35 import com.android.launcher3.util.Thunk;
36 import com.android.launcher3.views.ActivityContext;
37 import com.android.launcher3.views.BubbleTextHolder;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 public abstract class BaseAccessibilityDelegate<T extends Context & ActivityContext>
43         extends View.AccessibilityDelegate implements DragController.DragListener {
44 
45     public enum DragType {
46         ICON,
47         FOLDER,
48         APP_PAIR,
49         WIDGET
50     }
51 
52     public static class DragInfo {
53         public DragType dragType;
54         public ItemInfo info;
55         public View item;
56     }
57 
58     protected final SparseArray<LauncherAction> mActions = new SparseArray<>();
59     protected final T mContext;
60 
61     protected DragInfo mDragInfo = null;
62 
BaseAccessibilityDelegate(T context)63     protected BaseAccessibilityDelegate(T context) {
64         mContext = context;
65     }
66 
67     @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)68     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
69         super.onInitializeAccessibilityNodeInfo(host, info);
70         if (host.getTag() instanceof ItemInfo) {
71             ItemInfo item = (ItemInfo) host.getTag();
72 
73             List<LauncherAction> actions = new ArrayList<>();
74             getSupportedActions(host, item, actions);
75             actions.forEach(la -> info.addAction(la.accessibilityAction));
76 
77             if (!itemSupportsLongClick(host)) {
78                 info.setLongClickable(false);
79                 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
80             }
81         }
82     }
83 
84     /**
85      * Adds all the accessibility actions that can be handled.
86      */
getSupportedActions(View host, ItemInfo item, List<LauncherAction> out)87     protected abstract void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out);
88 
itemSupportsLongClick(View host)89     private boolean itemSupportsLongClick(View host) {
90         if (host instanceof BubbleTextView) {
91             return ((BubbleTextView) host).canShowLongPressPopup();
92         } else if (host instanceof BubbleTextHolder) {
93             BubbleTextHolder holder = (BubbleTextHolder) host;
94             return holder.getBubbleText() != null && holder.getBubbleText().canShowLongPressPopup();
95         } else {
96             return false;
97         }
98     }
99 
itemSupportsAccessibleDrag(ItemInfo item)100     protected boolean itemSupportsAccessibleDrag(ItemInfo item) {
101         if (item instanceof WorkspaceItemInfo) {
102             // Support the action unless the item is in a context menu.
103             return item.screenId >= 0
104                     && item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
105         }
106         return (item instanceof LauncherAppWidgetInfo)
107                 || (item instanceof CollectionInfo);
108     }
109 
110     @Override
performAccessibilityAction(View host, int action, Bundle args)111     public boolean performAccessibilityAction(View host, int action, Bundle args) {
112         if ((host.getTag() instanceof ItemInfo)
113                 && performAction(host, (ItemInfo) host.getTag(), action, false)) {
114             return true;
115         }
116         return super.performAccessibilityAction(host, action, args);
117     }
118 
performAction( View host, ItemInfo item, int action, boolean fromKeyboard)119     protected abstract boolean performAction(
120             View host, ItemInfo item, int action, boolean fromKeyboard);
121 
122     @Thunk
announceConfirmation(String confirmation)123     protected void announceConfirmation(String confirmation) {
124         mContext.getDragLayer().announceForAccessibility(confirmation);
125     }
126 
isInAccessibleDrag()127     public boolean isInAccessibleDrag() {
128         return mDragInfo != null;
129     }
130 
getDragInfo()131     public DragInfo getDragInfo() {
132         return mDragInfo;
133     }
134 
135     /**
136      * @param clickedTarget the actual view that was clicked
137      * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used
138      * as the actual drop location otherwise the views center is used.
139      */
handleAccessibleDrop(View clickedTarget, Rect dropLocation, String confirmation)140     public void handleAccessibleDrop(View clickedTarget, Rect dropLocation,
141             String confirmation) {
142         if (!isInAccessibleDrag()) return;
143 
144         int[] loc = new int[2];
145         if (dropLocation == null) {
146             loc[0] = clickedTarget.getWidth() / 2;
147             loc[1] = clickedTarget.getHeight() / 2;
148         } else {
149             loc[0] = dropLocation.centerX();
150             loc[1] = dropLocation.centerY();
151         }
152 
153         mContext.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc);
154         mContext.getDragController().completeAccessibleDrag(loc);
155 
156         if (!TextUtils.isEmpty(confirmation)) {
157             announceConfirmation(confirmation);
158         }
159     }
160 
beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard)161     protected abstract boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard);
162 
163 
164     @Override
onDragEnd()165     public void onDragEnd() {
166         mContext.getDragController().removeDragListener(this);
167         mDragInfo = null;
168     }
169 
170     @Override
onDragStart(DropTarget.DragObject dragObject, DragOptions options)171     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
172         // No-op
173     }
174 
175     public class LauncherAction {
176         public final int keyCode;
177         public final AccessibilityNodeInfo.AccessibilityAction accessibilityAction;
178 
179         private final BaseAccessibilityDelegate<T> mDelegate;
180 
LauncherAction(int id, int labelRes, int keyCode)181         public LauncherAction(int id, int labelRes, int keyCode) {
182             this.keyCode = keyCode;
183             accessibilityAction = new AccessibilityNodeInfo.AccessibilityAction(
184                     id, mContext.getString(labelRes));
185             mDelegate = BaseAccessibilityDelegate.this;
186         }
187 
188         /**
189          * Invokes the action for the provided host
190          */
invokeFromKeyboard(View host)191         public boolean invokeFromKeyboard(View host) {
192             if (host != null && host.getTag() instanceof ItemInfo) {
193                 return mDelegate.performAction(
194                         host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true);
195             } else {
196                 return false;
197             }
198         }
199     }
200 }
201