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 import com.android.launcher3.dragndrop.DragLayer;
35 
36 import java.util.List;
37 
38 /**
39  * Helper class to make drag-and-drop in a {@link CellLayout} accessible.
40  */
41 public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper
42         implements OnClickListener, OnHoverListener {
43     protected static final int INVALID_POSITION = -1;
44 
45     protected final Rect mTempRect = new Rect();
46     protected final int[] mTempCords = new int[2];
47 
48     protected final CellLayout mView;
49     protected final Context mContext;
50     protected final LauncherAccessibilityDelegate mDelegate;
51     protected final DragLayer mDragLayer;
52 
DragAndDropAccessibilityDelegate(CellLayout forView)53     public DragAndDropAccessibilityDelegate(CellLayout forView) {
54         super(forView);
55         mView = forView;
56         mContext = mView.getContext();
57         Launcher launcher = Launcher.getLauncher(mContext);
58         mDelegate = launcher.getAccessibilityDelegate();
59         mDragLayer = launcher.getDragLayer();
60     }
61 
62     @Override
getVirtualViewAt(float x, float y)63     public int getVirtualViewAt(float x, float y) {
64         if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) {
65             return INVALID_ID;
66         }
67         mView.pointToCellExact((int) x, (int) y, mTempCords);
68 
69         // Map cell to id
70         int id = mTempCords[0] + mTempCords[1] * mView.getCountX();
71         return intersectsValidDropTarget(id);
72     }
73 
74     /**
75      * @return the view id of the top left corner of a valid drop region or
76      * {@link #INVALID_POSITION} if there is no such valid region.
77      */
intersectsValidDropTarget(int id)78     protected abstract int intersectsValidDropTarget(int id);
79 
80     @Override
getVisibleVirtualViews(List<Integer> virtualViews)81     public void getVisibleVirtualViews(List<Integer> virtualViews) {
82         // We create a virtual view for each cell of the grid
83         // The cell ids correspond to cells in reading order.
84         int nCells = mView.getCountX() * mView.getCountY();
85 
86         for (int i = 0; i < nCells; i++) {
87             if (intersectsValidDropTarget(i) == i) {
88                 virtualViews.add(i);
89             }
90         }
91     }
92 
93     @Override
onPerformActionForVirtualView(int viewId, int action, Bundle args)94     public boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
95         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) {
96             String confirmation = getConfirmationForIconDrop(viewId);
97             mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation);
98             return true;
99         }
100         return false;
101     }
102 
103     @Override
onClick(View v)104     public void onClick(View v) {
105         onPerformActionForVirtualView(getFocusedVirtualView(),
106                 AccessibilityNodeInfoCompat.ACTION_CLICK, null);
107     }
108 
109     @Override
onPopulateEventForVirtualView(int id, AccessibilityEvent event)110     protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) {
111         if (id == INVALID_ID) {
112             throw new IllegalArgumentException("Invalid virtual view id");
113         }
114         event.setContentDescription(mContext.getString(R.string.action_move_here));
115     }
116 
117     @Override
onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node)118     public void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
119         if (id == INVALID_ID) {
120             throw new IllegalArgumentException("Invalid virtual view id");
121         }
122 
123         node.setContentDescription(getLocationDescriptionForIconDrop(id));
124 
125         Rect itemBounds = getItemBounds(id);
126         node.setBoundsInParent(itemBounds);
127 
128         // ExploreByTouchHelper does not currently handle view scale.
129         // Update BoundsInScreen to appropriate value.
130         mTempCords[0] = mTempCords[1] = 0;
131         float scale = mDragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
132         mTempRect.left = mTempCords[0] + (int) (itemBounds.left * scale);
133         mTempRect.right = mTempCords[0] + (int) (itemBounds.right * scale);
134         mTempRect.top = mTempCords[1] + (int) (itemBounds.top * scale);
135         mTempRect.bottom = mTempCords[1] + (int) (itemBounds.bottom * scale);
136         node.setBoundsInScreen(mTempRect);
137 
138         node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
139         node.setClickable(true);
140         node.setFocusable(true);
141     }
142 
143     @Override
onHover(View view, MotionEvent motionEvent)144     public boolean onHover(View view, MotionEvent motionEvent) {
145         return dispatchHoverEvent(motionEvent);
146     }
147 
148     /**
149      * Returns the target host container
150      */
getHost()151     public View getHost() {
152         return mView;
153     }
154 
getLocationDescriptionForIconDrop(int id)155     protected abstract String getLocationDescriptionForIconDrop(int id);
156 
getConfirmationForIconDrop(int id)157     protected abstract String getConfirmationForIconDrop(int id);
158 
getItemBounds(int id)159     private Rect getItemBounds(int id) {
160         int cellX = id % mView.getCountX();
161         int cellY = id / mView.getCountX();
162         LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo();
163         mView.cellToRect(cellX, cellY, dragInfo.info.spanX, dragInfo.info.spanY, mTempRect);
164         return mTempRect;
165     }
166 }