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.keyboard;
17 
18 import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL;
19 
20 import static com.android.launcher3.LauncherState.SPRING_LOADED;
21 
22 import android.app.Activity;
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.Paint.Style;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.util.AttributeSet;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewParent;
33 import android.widget.TextView;
34 
35 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
36 
37 import com.android.launcher3.AbstractFloatingView;
38 import com.android.launcher3.CellLayout;
39 import com.android.launcher3.Insettable;
40 import com.android.launcher3.Launcher;
41 import com.android.launcher3.LauncherState;
42 import com.android.launcher3.PagedView;
43 import com.android.launcher3.R;
44 import com.android.launcher3.Utilities;
45 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
46 import com.android.launcher3.dragndrop.DragOptions;
47 import com.android.launcher3.folder.Folder;
48 import com.android.launcher3.model.data.ItemInfo;
49 import com.android.launcher3.statemanager.StateManager.StateListener;
50 import com.android.launcher3.touch.ItemLongClickListener;
51 import com.android.launcher3.util.Themes;
52 
53 import java.util.ArrayList;
54 import java.util.Objects;
55 import java.util.function.ToIntBiFunction;
56 import java.util.function.ToIntFunction;
57 
58 /**
59  * A floating view to allow keyboard navigation across virtual nodes
60  */
61 public class KeyboardDragAndDropView extends AbstractFloatingView
62         implements Insettable, StateListener<LauncherState> {
63 
64     private static final long MINOR_AXIS_WEIGHT = 13;
65 
66     private final ArrayList<Integer> mIntList = new ArrayList<>();
67     private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
68     private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>();
69 
70     private final Rect mTempRect = new Rect();
71     private final Rect mTempRect2 = new Rect();
72     private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain();
73 
74     private final RectFocusIndicator mFocusIndicator;
75 
76     private final Launcher mLauncher;
77     private VirtualNodeInfo mCurrentSelection;
78 
79 
KeyboardDragAndDropView(Context context, AttributeSet attrs)80     public KeyboardDragAndDropView(Context context, AttributeSet attrs) {
81         this(context, attrs, 0);
82     }
83 
KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr)84     public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) {
85         super(context, attrs, defStyleAttr);
86         mLauncher = Launcher.getLauncher(context);
87         mFocusIndicator = new RectFocusIndicator(this);
88         setWillNotDraw(false);
89     }
90 
91     @Override
handleClose(boolean animate)92     protected void handleClose(boolean animate) {
93         mLauncher.getDragLayer().removeView(this);
94         mLauncher.getStateManager().removeStateListener(this);
95         mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
96         mIsOpen = false;
97     }
98 
99     @Override
isOfType(int type)100     protected boolean isOfType(int type) {
101         return (type & TYPE_DRAG_DROP_POPUP) != 0;
102     }
103 
104     @Override
onControllerInterceptTouchEvent(MotionEvent ev)105     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
106         // Consume all touch
107         return true;
108     }
109 
110     @Override
setInsets(Rect insets)111     public void setInsets(Rect insets) {
112         setPadding(insets.left, insets.top, insets.right, insets.bottom);
113     }
114 
115     @Override
onStateTransitionStart(LauncherState toState)116     public void onStateTransitionStart(LauncherState toState) {
117         if (toState != SPRING_LOADED) {
118             close(false);
119         }
120     }
121 
122     @Override
onStateTransitionComplete(LauncherState finalState)123     public void onStateTransitionComplete(LauncherState finalState) {
124         if (mCurrentSelection != null) {
125             setCurrentSelection(mCurrentSelection);
126         }
127     }
128 
setCurrentSelection(VirtualNodeInfo nodeInfo)129     private void setCurrentSelection(VirtualNodeInfo nodeInfo) {
130         mCurrentSelection = nodeInfo;
131         ((TextView) findViewById(R.id.label))
132                 .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription());
133 
134         Rect bounds = new Rect();
135         mTempNodeInfo.getBoundsInParent(bounds);
136         View host = nodeInfo.delegate.getHost();
137         ViewParent parent = host.getParent();
138         if (parent instanceof PagedView) {
139             PagedView pv = (PagedView) parent;
140             int pageIndex = pv.indexOfChild(host);
141 
142             pv.setCurrentPage(pageIndex);
143             bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0);
144         }
145         float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom};
146         Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true);
147 
148         new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds);
149         mFocusIndicator.changeFocus(bounds, true);
150     }
151 
152     @Override
onDraw(Canvas canvas)153     protected void onDraw(Canvas canvas) {
154         mFocusIndicator.draw(canvas);
155     }
156 
157     @Override
dispatchUnhandledMove(View focused, int direction)158     public boolean dispatchUnhandledMove(View focused, int direction) {
159         VirtualNodeInfo nodeInfo = getNextSelection(direction);
160         if (nodeInfo == null) {
161             return false;
162         }
163         setCurrentSelection(nodeInfo);
164         return true;
165     }
166 
167     /**
168      * Focus finding logic:
169      * Collect all virtual nodes in reading order (used for forward and backwards).
170      * Then find the closest view by comparing the distances spatially. Since it is a move
171      * operation. consider all cell sizes to be approximately of the same size.
172      */
getNextSelection(int direction)173     private VirtualNodeInfo getNextSelection(int direction) {
174         // Collect all virtual nodes
175         mDelegates.clear();
176         mNodes.clear();
177 
178         Folder openFolder = Folder.getOpen(mLauncher);
179         PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent();
180         int count = pv.getPageCount();
181         for (int i = 0; i < count; i++) {
182             mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate());
183         }
184         if (openFolder == null) {
185             mDelegates.add(pv.getNextPage() + 1,
186                     mLauncher.getHotseat().getDragAndDropAccessibilityDelegate());
187         }
188         mDelegates.forEach(delegate -> {
189             mIntList.clear();
190             delegate.getVisibleVirtualViews(mIntList);
191             mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id)));
192         });
193 
194         if (mNodes.isEmpty()) {
195             return null;
196         }
197         int index = mNodes.indexOf(mCurrentSelection);
198         if (mCurrentSelection == null || index < 0) {
199             return null;
200         }
201         int totalNodes = mNodes.size();
202 
203         final ToIntBiFunction<Rect, Rect> majorAxis;
204         final ToIntFunction<Rect> minorAxis;
205 
206         switch (direction) {
207             case View.FOCUS_RIGHT:
208                 majorAxis = (source, dest) -> dest.left - source.left;
209                 minorAxis = Rect::centerY;
210                 break;
211             case View.FOCUS_LEFT:
212                 majorAxis = (source, dest) -> source.left - dest.left;
213                 minorAxis = Rect::centerY;
214                 break;
215             case View.FOCUS_UP:
216                 majorAxis = (source, dest) -> source.top - dest.top;
217                 minorAxis = Rect::centerX;
218                 break;
219             case View.FOCUS_DOWN:
220                 majorAxis = (source, dest) -> dest.top - source.top;
221                 minorAxis = Rect::centerX;
222                 break;
223             case View.FOCUS_FORWARD:
224                 return mNodes.get((index + 1) % totalNodes);
225             case View.FOCUS_BACKWARD:
226                 return mNodes.get((index + totalNodes - 1) % totalNodes);
227             default:
228                 // Unknown direction
229                 return null;
230         }
231         mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect);
232 
233         float minWeight = Float.MAX_VALUE;
234         VirtualNodeInfo match = null;
235         for (int i = 0; i < totalNodes; i++) {
236             VirtualNodeInfo node = mNodes.get(i);
237             node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2);
238 
239             int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2);
240             if (majorAxisWeight <= 0) {
241                 continue;
242             }
243             int minorAxisWeight = minorAxis.applyAsInt(mTempRect2)
244                     - minorAxis.applyAsInt(mTempRect);
245 
246             float weight = majorAxisWeight * majorAxisWeight
247                     + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT;
248             if (weight < minWeight) {
249                 minWeight = weight;
250                 match = node;
251             }
252         }
253         return match;
254     }
255 
256     @Override
onKeyUp(int keyCode, KeyEvent event)257     public boolean onKeyUp(int keyCode, KeyEvent event) {
258         if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) {
259             mCurrentSelection.delegate.onPerformActionForVirtualView(
260                     mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
261             return true;
262         }
263         return super.onKeyUp(keyCode, event);
264     }
265 
266     /**
267      * Shows the keyboard drag popup for the provided view
268      */
showForIcon(View icon, ItemInfo item, DragOptions dragOptions)269     public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) {
270         mIsOpen = true;
271         mLauncher.getDragLayer().addView(this);
272         mLauncher.getStateManager().addStateListener(this);
273 
274         // Find current selection
275         CellLayout currentParent = (CellLayout) icon.getParent().getParent();
276         float[] iconPos = new float[] {currentParent.getCellWidth() / 2,
277                 currentParent.getCellHeight() / 2};
278         Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false);
279 
280         ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions);
281 
282         DragAndDropAccessibilityDelegate dndDelegate =
283                 currentParent.getDragAndDropAccessibilityDelegate();
284         setCurrentSelection(new VirtualNodeInfo(
285                 dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1])));
286 
287         mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE);
288         requestFocus();
289     }
290 
291     private static class VirtualNodeInfo {
292         public final DragAndDropAccessibilityDelegate delegate;
293         public final int id;
294 
VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id)295         VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) {
296             this.id = id;
297             this.delegate = delegate;
298         }
299 
300         @Override
equals(Object o)301         public boolean equals(Object o) {
302             if (this == o) {
303                 return true;
304             }
305             if (!(o instanceof VirtualNodeInfo)) {
306                 return false;
307             }
308             VirtualNodeInfo that = (VirtualNodeInfo) o;
309             return id == that.id && delegate.equals(that.delegate);
310         }
311 
populate(AccessibilityNodeInfoCompat nodeInfo)312         public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) {
313             delegate.onPopulateNodeForVirtualView(id, nodeInfo);
314             return nodeInfo;
315         }
316 
getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out)317         public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) {
318             delegate.onPopulateNodeForVirtualView(id, nodeInfo);
319             nodeInfo.getBoundsInScreen(out);
320         }
321 
322         @Override
hashCode()323         public int hashCode() {
324             return Objects.hash(id, delegate);
325         }
326     }
327 
328     private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> {
329 
RectFocusIndicator(View container)330         RectFocusIndicator(View container) {
331             super(container, Themes.getColorAccent(container.getContext()));
332             mPaint.setStrokeWidth(container.getResources()
333                     .getDimension(R.dimen.keyboard_drag_stroke_width));
334             mPaint.setStyle(Style.STROKE);
335         }
336 
337         @Override
viewToRect(Rect item, Rect outRect)338         public void viewToRect(Rect item, Rect outRect) {
339             outRect.set(item);
340         }
341     }
342 }
343