1 /*
2  * Copyright (C) 2015 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.launcher3.accessibility;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.View.OnClickListener;
25 import android.view.View.OnHoverListener;
26 import android.view.accessibility.AccessibilityEvent;
27 
28 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
29 import androidx.customview.widget.ExploreByTouchHelper;
30 
31 import com.android.launcher3.CellLayout;
32 import com.android.launcher3.Launcher;
33 import com.android.launcher3.R;
34 
35 import java.util.List;
36 
37 /**
38  * Helper class to make drag-and-drop in a {@link CellLayout} accessible.
39  */
40 public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper
41         implements OnClickListener, OnHoverListener {
42     protected static final int INVALID_POSITION = -1;
43 
44     private static final int[] sTempArray = new int[2];
45 
46     protected final CellLayout mView;
47     protected final Context mContext;
48     protected final LauncherAccessibilityDelegate mDelegate;
49 
50     private final Rect mTempRect = new Rect();
51 
DragAndDropAccessibilityDelegate(CellLayout forView)52     public DragAndDropAccessibilityDelegate(CellLayout forView) {
53         super(forView);
54         mView = forView;
55         mContext = mView.getContext();
56         mDelegate = Launcher.getLauncher(mContext).getAccessibilityDelegate();
57     }
58 
59     @Override
getVirtualViewAt(float x, float y)60     protected int getVirtualViewAt(float x, float y) {
61         if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) {
62             return INVALID_ID;
63         }
64         mView.pointToCellExact((int) x, (int) y, sTempArray);
65 
66         // Map cell to id
67         int id = sTempArray[0] + sTempArray[1] * mView.getCountX();
68         return intersectsValidDropTarget(id);
69     }
70 
71     /**
72      * @return the view id of the top left corner of a valid drop region or
73      * {@link #INVALID_POSITION} if there is no such valid region.
74      */
intersectsValidDropTarget(int id)75     protected abstract int intersectsValidDropTarget(int id);
76 
77     @Override
getVisibleVirtualViews(List<Integer> virtualViews)78     protected void getVisibleVirtualViews(List<Integer> virtualViews) {
79         // We create a virtual view for each cell of the grid
80         // The cell ids correspond to cells in reading order.
81         int nCells = mView.getCountX() * mView.getCountY();
82 
83         for (int i = 0; i < nCells; i++) {
84             if (intersectsValidDropTarget(i) == i) {
85                 virtualViews.add(i);
86             }
87         }
88     }
89 
90     @Override
onPerformActionForVirtualView(int viewId, int action, Bundle args)91     protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
92         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) {
93             String confirmation = getConfirmationForIconDrop(viewId);
94             mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation);
95             return true;
96         }
97         return false;
98     }
99 
100     @Override
onClick(View v)101     public void onClick(View v) {
102         onPerformActionForVirtualView(getFocusedVirtualView(),
103                 AccessibilityNodeInfoCompat.ACTION_CLICK, null);
104     }
105 
106     @Override
onPopulateEventForVirtualView(int id, AccessibilityEvent event)107     protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) {
108         if (id == INVALID_ID) {
109             throw new IllegalArgumentException("Invalid virtual view id");
110         }
111         event.setContentDescription(mContext.getString(R.string.action_move_here));
112     }
113 
114     @Override
onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node)115     protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
116         if (id == INVALID_ID) {
117             throw new IllegalArgumentException("Invalid virtual view id");
118         }
119 
120         node.setContentDescription(getLocationDescriptionForIconDrop(id));
121         node.setBoundsInParent(getItemBounds(id));
122 
123         node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
124         node.setClickable(true);
125         node.setFocusable(true);
126     }
127 
128     @Override
onHover(View view, MotionEvent motionEvent)129     public boolean onHover(View view, MotionEvent motionEvent) {
130         return dispatchHoverEvent(motionEvent);
131     }
132 
getLocationDescriptionForIconDrop(int id)133     protected abstract String getLocationDescriptionForIconDrop(int id);
134 
getConfirmationForIconDrop(int id)135     protected abstract String getConfirmationForIconDrop(int id);
136 
getItemBounds(int id)137     private Rect getItemBounds(int id) {
138         int cellX = id % mView.getCountX();
139         int cellY = id / mView.getCountX();
140         LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo();
141         mView.cellToRect(cellX, cellY, dragInfo.info.spanX, dragInfo.info.spanY, mTempRect);
142         return mTempRect;
143     }
144 }