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