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