1 /*
2  * Copyright (C) 2008 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.dragndrop;
18 
19 import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
20 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
21 import static com.android.launcher3.LauncherState.NORMAL;
22 import static com.android.launcher3.Utilities.ATLEAST_Q;
23 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
24 
25 import android.animation.ValueAnimator;
26 import android.content.ComponentName;
27 import android.content.res.Resources;
28 import android.graphics.Bitmap;
29 import android.graphics.Point;
30 import android.graphics.Rect;
31 import android.view.DragEvent;
32 import android.view.HapticFeedbackConstants;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.View;
36 
37 import com.android.launcher3.AbstractFloatingView;
38 import com.android.launcher3.DragSource;
39 import com.android.launcher3.DropTarget;
40 import com.android.launcher3.Launcher;
41 import com.android.launcher3.R;
42 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.WorkspaceItemInfo;
45 import com.android.launcher3.util.ItemInfoMatcher;
46 import com.android.launcher3.util.TouchController;
47 
48 import java.util.ArrayList;
49 
50 /**
51  * Class for initiating a drag within a view or across multiple views.
52  */
53 public class DragController implements DragDriver.EventListener, TouchController {
54     private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
55 
56     /**
57      * When a drag is started from a deep press, you need to drag this much farther than normal to
58      * end a pre-drag. See {@link DragOptions.PreDragCondition#shouldStartDrag(double)}.
59      */
60     private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
61 
62     private final Launcher mLauncher;
63     private final FlingToDeleteHelper mFlingToDeleteHelper;
64 
65     // temporaries to avoid gc thrash
66     private final Rect mRectTemp = new Rect();
67     private final int[] mCoordinatesTemp = new int[2];
68 
69     /**
70      * Drag driver for the current drag/drop operation, or null if there is no active DND operation.
71      * It's null during accessible drag operations.
72      */
73     private DragDriver mDragDriver = null;
74 
75     /** Options controlling the drag behavior. */
76     private DragOptions mOptions;
77 
78     /** Coordinate for motion down event */
79     private final Point mMotionDown = new Point();
80     /** Coordinate for last touch event **/
81     private final Point mLastTouch = new Point();
82 
83     private final Point mTmpPoint = new Point();
84 
85     private DropTarget.DragObject mDragObject;
86 
87     /** Who can receive drop events */
88     private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
89     private final ArrayList<DragListener> mListeners = new ArrayList<>();
90 
91     private DropTarget mLastDropTarget;
92 
93     private int mLastTouchClassification;
94     private int mDistanceSinceScroll = 0;
95 
96     private boolean mIsInPreDrag;
97 
98     /**
99      * Interface to receive notifications when a drag starts or stops
100      */
101     public interface DragListener {
102         /**
103          * A drag has begun
104          *
105          * @param dragObject The object being dragged
106          * @param options Options used to start the drag
107          */
onDragStart(DropTarget.DragObject dragObject, DragOptions options)108         void onDragStart(DropTarget.DragObject dragObject, DragOptions options);
109 
110         /**
111          * The drag has ended
112          */
onDragEnd()113         void onDragEnd();
114     }
115 
116     /**
117      * Used to create a new DragLayer from XML.
118      */
DragController(Launcher launcher)119     public DragController(Launcher launcher) {
120         mLauncher = launcher;
121         mFlingToDeleteHelper = new FlingToDeleteHelper(launcher);
122     }
123 
124     /**
125      * Starts a drag.
126      * When the drag is started, the UI automatically goes into spring loaded mode. On a successful
127      * drop, it is the responsibility of the {@link DropTarget} to exit out of the spring loaded
128      * mode. If the drop was cancelled for some reason, the UI will automatically exit out of this mode.
129      *
130      * @param b The bitmap to display as the drag image.  It will be re-scaled to the
131      *          enlarged size.
132      * @param originalView The source view (ie. icon, widget etc.) that is being dragged
133      *          and which the DragView represents
134      * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
135      * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
136      * @param source An object representing where the drag originated
137      * @param dragInfo The data associated with the object that is being dragged
138      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
139      *          Makes dragging feel more precise, e.g. you can clip out a transparent border
140      */
startDrag(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)141     public DragView startDrag(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY,
142             DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
143             float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
144         if (PROFILE_DRAWING_DURING_DRAG) {
145             android.os.Debug.startMethodTracing("Launcher");
146         }
147 
148         mLauncher.hideKeyboard();
149         AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_DISCOVERY_BOUNCE);
150 
151         mOptions = options;
152         if (mOptions.simulatedDndStartPoint != null) {
153             mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
154             mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
155         }
156 
157         final int registrationX = mMotionDown.x - dragLayerX;
158         final int registrationY = mMotionDown.y - dragLayerY;
159 
160         final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
161         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
162 
163         mLastDropTarget = null;
164 
165         mDragObject = new DropTarget.DragObject(mLauncher.getApplicationContext());
166         mDragObject.originalView = originalView;
167 
168         mIsInPreDrag = mOptions.preDragCondition != null
169                 && !mOptions.preDragCondition.shouldStartDrag(0);
170 
171         final Resources res = mLauncher.getResources();
172         final float scaleDps = mIsInPreDrag
173                 ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
174         final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
175                 registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
176         dragView.setItemInfo(dragInfo);
177         mDragObject.dragComplete = false;
178 
179         mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
180         mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
181 
182         mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
183         if (!mOptions.isAccessibleDrag) {
184             mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
185         }
186 
187         mDragObject.dragSource = source;
188         mDragObject.dragInfo = dragInfo;
189         mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
190 
191         if (dragOffset != null) {
192             dragView.setDragVisualizeOffset(new Point(dragOffset));
193         }
194         if (dragRegion != null) {
195             dragView.setDragRegion(new Rect(dragRegion));
196         }
197 
198         mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
199         dragView.show(mLastTouch.x, mLastTouch.y);
200         mDistanceSinceScroll = 0;
201 
202         if (!mIsInPreDrag) {
203             callOnDragStart();
204         } else if (mOptions.preDragCondition != null) {
205             mOptions.preDragCondition.onPreDragStart(mDragObject);
206         }
207 
208         handleMoveEvent(mLastTouch.x, mLastTouch.y);
209         mLauncher.getUserEventDispatcher().resetActionDurationMillis();
210 
211         if (!mLauncher.isTouchInProgress() && options.simulatedDndStartPoint == null) {
212             // If it is an internal drag and the touch is already complete, cancel immediately
213             MAIN_EXECUTOR.submit(this::cancelDrag);
214         }
215         return dragView;
216     }
217 
callOnDragStart()218     private void callOnDragStart() {
219         if (mOptions.preDragCondition != null) {
220             mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/);
221         }
222         mIsInPreDrag = false;
223         for (DragListener listener : new ArrayList<>(mListeners)) {
224             listener.onDragStart(mDragObject, mOptions);
225         }
226     }
227 
addFirstFrameAnimationHelper(ValueAnimator anim)228     public void addFirstFrameAnimationHelper(ValueAnimator anim) {
229         if (mDragObject != null && mDragObject.dragView != null) {
230             mDragObject.dragView.mFirstFrameAnimatorHelper.addTo(anim);
231         }
232     }
233 
234     /**
235      * Call this from a drag source view like this:
236      *
237      * <pre>
238      *  @Override
239      *  public boolean dispatchKeyEvent(KeyEvent event) {
240      *      return mDragController.dispatchKeyEvent(this, event)
241      *              || super.dispatchKeyEvent(event);
242      * </pre>
243      */
dispatchKeyEvent(KeyEvent event)244     public boolean dispatchKeyEvent(KeyEvent event) {
245         return mDragDriver != null;
246     }
247 
isDragging()248     public boolean isDragging() {
249         return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag);
250     }
251 
252     /**
253      * Stop dragging without dropping.
254      */
cancelDrag()255     public void cancelDrag() {
256         if (isDragging()) {
257             if (mLastDropTarget != null) {
258                 mLastDropTarget.onDragExit(mDragObject);
259             }
260             mDragObject.deferDragViewCleanupPostAnimation = false;
261             mDragObject.cancelled = true;
262             mDragObject.dragComplete = true;
263             if (!mIsInPreDrag) {
264                 dispatchDropComplete(null, false);
265             }
266         }
267         endDrag();
268     }
269 
dispatchDropComplete(View dropTarget, boolean accepted)270     private void dispatchDropComplete(View dropTarget, boolean accepted) {
271         if (!accepted) {
272             // If it was not accepted, cleanup the state. If it was accepted, it is the
273             // responsibility of the drop target to cleanup the state.
274             mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
275             mDragObject.deferDragViewCleanupPostAnimation = false;
276         }
277 
278         mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted);
279     }
280 
onAppsRemoved(ItemInfoMatcher matcher)281     public void onAppsRemoved(ItemInfoMatcher matcher) {
282         // Cancel the current drag if we are removing an app that we are dragging
283         if (mDragObject != null) {
284             ItemInfo dragInfo = mDragObject.dragInfo;
285             if (dragInfo instanceof WorkspaceItemInfo) {
286                 ComponentName cn = dragInfo.getTargetComponent();
287                 if (cn != null && matcher.matches(dragInfo, cn)) {
288                     cancelDrag();
289                 }
290             }
291         }
292     }
293 
endDrag()294     private void endDrag() {
295         if (isDragging()) {
296             mDragDriver = null;
297             boolean isDeferred = false;
298             if (mDragObject.dragView != null) {
299                 isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
300                 if (!isDeferred) {
301                     mDragObject.dragView.remove();
302                 } else if (mIsInPreDrag) {
303                     animateDragViewToOriginalPosition(null, null, -1);
304                 }
305                 mDragObject.dragView = null;
306             }
307 
308             // Only end the drag if we are not deferred
309             if (!isDeferred) {
310                 callOnDragEnd();
311             }
312         }
313 
314         mFlingToDeleteHelper.releaseVelocityTracker();
315     }
316 
animateDragViewToOriginalPosition(final Runnable onComplete, final View originalIcon, int duration)317     public void animateDragViewToOriginalPosition(final Runnable onComplete,
318             final View originalIcon, int duration) {
319         Runnable onCompleteRunnable = new Runnable() {
320             @Override
321             public void run() {
322                 if (originalIcon != null) {
323                     originalIcon.setVisibility(View.VISIBLE);
324                 }
325                 if (onComplete != null) {
326                     onComplete.run();
327                 }
328             }
329         };
330         mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration);
331     }
332 
callOnDragEnd()333     private void callOnDragEnd() {
334         if (mIsInPreDrag && mOptions.preDragCondition != null) {
335             mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/);
336         }
337         mIsInPreDrag = false;
338         mOptions = null;
339         for (DragListener listener : new ArrayList<>(mListeners)) {
340             listener.onDragEnd();
341         }
342     }
343 
344     /**
345      * This only gets called as a result of drag view cleanup being deferred in endDrag();
346      */
onDeferredEndDrag(DragView dragView)347     void onDeferredEndDrag(DragView dragView) {
348         dragView.remove();
349 
350         if (mDragObject.deferDragViewCleanupPostAnimation) {
351             // If we skipped calling onDragEnd() before, do it now
352             callOnDragEnd();
353         }
354     }
355 
356     /**
357      * Clamps the position to the drag layer bounds.
358      */
getClampedDragLayerPos(float x, float y)359     private Point getClampedDragLayerPos(float x, float y) {
360         mLauncher.getDragLayer().getLocalVisibleRect(mRectTemp);
361         mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1));
362         mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1));
363         return mTmpPoint;
364     }
365 
366     @Override
onDriverDragMove(float x, float y)367     public void onDriverDragMove(float x, float y) {
368         Point dragLayerPos = getClampedDragLayerPos(x, y);
369         handleMoveEvent(dragLayerPos.x, dragLayerPos.y);
370     }
371 
372     @Override
onDriverDragExitWindow()373     public void onDriverDragExitWindow() {
374         if (mLastDropTarget != null) {
375             mLastDropTarget.onDragExit(mDragObject);
376             mLastDropTarget = null;
377         }
378     }
379 
380     @Override
onDriverDragEnd(float x, float y)381     public void onDriverDragEnd(float x, float y) {
382         DropTarget dropTarget;
383         Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject, mOptions);
384         if (flingAnimation != null) {
385             dropTarget = mFlingToDeleteHelper.getDropTarget();
386         } else {
387             dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp);
388         }
389 
390         drop(dropTarget, flingAnimation);
391 
392         endDrag();
393     }
394 
395     @Override
onDriverDragCancel()396     public void onDriverDragCancel() {
397         cancelDrag();
398     }
399 
400     /**
401      * Call this from a drag source view.
402      */
403     @Override
onControllerInterceptTouchEvent(MotionEvent ev)404     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
405         if (mOptions != null && mOptions.isAccessibleDrag) {
406             return false;
407         }
408 
409         Point dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
410         mLastTouch.set(dragLayerPos.x,  dragLayerPos.y);
411         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
412             // Remember location of down touch
413             mMotionDown.set(dragLayerPos.x,  dragLayerPos.y);
414         }
415 
416         if (ATLEAST_Q) {
417             mLastTouchClassification = ev.getClassification();
418         }
419         return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
420     }
421 
422     /**
423      * Call this from a drag source view.
424      */
425     @Override
onControllerTouchEvent(MotionEvent ev)426     public boolean onControllerTouchEvent(MotionEvent ev) {
427         return mDragDriver != null && mDragDriver.onTouchEvent(ev);
428     }
429 
430     /**
431      * Call this from a drag source view.
432      */
onDragEvent(DragEvent event)433     public boolean onDragEvent(DragEvent event) {
434         return mDragDriver != null && mDragDriver.onDragEvent(event);
435     }
436 
handleMoveEvent(int x, int y)437     private void handleMoveEvent(int x, int y) {
438         mDragObject.dragView.move(x, y);
439 
440         // Drop on someone?
441         final int[] coordinates = mCoordinatesTemp;
442         DropTarget dropTarget = findDropTarget(x, y, coordinates);
443         mDragObject.x = coordinates[0];
444         mDragObject.y = coordinates[1];
445         checkTouchMove(dropTarget);
446 
447         // Check if we are hovering over the scroll areas
448         mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y);
449         mLastTouch.set(x, y);
450 
451         int distanceDragged = mDistanceSinceScroll;
452         if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
453             distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR;
454         }
455         if (mIsInPreDrag && mOptions.preDragCondition != null
456                 && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) {
457             callOnDragStart();
458         }
459     }
460 
getDistanceDragged()461     public float getDistanceDragged() {
462         return mDistanceSinceScroll;
463     }
464 
forceTouchMove()465     public void forceTouchMove() {
466         int[] dummyCoordinates = mCoordinatesTemp;
467         DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, dummyCoordinates);
468         mDragObject.x = dummyCoordinates[0];
469         mDragObject.y = dummyCoordinates[1];
470         checkTouchMove(dropTarget);
471     }
472 
checkTouchMove(DropTarget dropTarget)473     private void checkTouchMove(DropTarget dropTarget) {
474         if (dropTarget != null) {
475             if (mLastDropTarget != dropTarget) {
476                 if (mLastDropTarget != null) {
477                     mLastDropTarget.onDragExit(mDragObject);
478                 }
479                 dropTarget.onDragEnter(mDragObject);
480             }
481             dropTarget.onDragOver(mDragObject);
482         } else {
483             if (mLastDropTarget != null) {
484                 mLastDropTarget.onDragExit(mDragObject);
485             }
486         }
487         mLastDropTarget = dropTarget;
488     }
489 
490     /**
491      * As above, since accessible drag and drop won't cause the same sequence of touch events,
492      * we manually ensure appropriate drag and drop events get emulated for accessible drag.
493      */
completeAccessibleDrag(int[] location)494     public void completeAccessibleDrag(int[] location) {
495         final int[] coordinates = mCoordinatesTemp;
496 
497         // We make sure that we prime the target for drop.
498         DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates);
499         mDragObject.x = coordinates[0];
500         mDragObject.y = coordinates[1];
501         checkTouchMove(dropTarget);
502 
503         dropTarget.prepareAccessibilityDrop();
504         // Perform the drop
505         drop(dropTarget, null);
506         endDrag();
507     }
508 
drop(DropTarget dropTarget, Runnable flingAnimation)509     private void drop(DropTarget dropTarget, Runnable flingAnimation) {
510         final int[] coordinates = mCoordinatesTemp;
511         mDragObject.x = coordinates[0];
512         mDragObject.y = coordinates[1];
513 
514         // Move dragging to the final target.
515         if (dropTarget != mLastDropTarget) {
516             if (mLastDropTarget != null) {
517                 mLastDropTarget.onDragExit(mDragObject);
518             }
519             mLastDropTarget = dropTarget;
520             if (dropTarget != null) {
521                 dropTarget.onDragEnter(mDragObject);
522             }
523         }
524 
525         mDragObject.dragComplete = true;
526         if (mIsInPreDrag) {
527             if (dropTarget != null) {
528                 dropTarget.onDragExit(mDragObject);
529             }
530             return;
531         }
532 
533         // Drop onto the target.
534         boolean accepted = false;
535         if (dropTarget != null) {
536             dropTarget.onDragExit(mDragObject);
537             if (dropTarget.acceptDrop(mDragObject)) {
538                 if (flingAnimation != null) {
539                     flingAnimation.run();
540                 } else {
541                     dropTarget.onDrop(mDragObject, mOptions);
542                 }
543                 accepted = true;
544             }
545         }
546         final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
547         mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
548         dispatchDropComplete(dropTargetAsView, accepted);
549     }
550 
findDropTarget(int x, int y, int[] dropCoordinates)551     private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
552         mDragObject.x = x;
553         mDragObject.y = y;
554 
555         final Rect r = mRectTemp;
556         final ArrayList<DropTarget> dropTargets = mDropTargets;
557         final int count = dropTargets.size();
558         for (int i = count - 1; i >= 0; i--) {
559             DropTarget target = dropTargets.get(i);
560             if (!target.isDropEnabled())
561                 continue;
562 
563             target.getHitRectRelativeToDragLayer(r);
564             if (r.contains(x, y)) {
565                 dropCoordinates[0] = x;
566                 dropCoordinates[1] = y;
567                 mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
568                 return target;
569             }
570         }
571         // Pass all unhandled drag to workspace. Workspace finds the correct
572         // cell layout to drop to in the existing drag/drop logic.
573         dropCoordinates[0] = x;
574         dropCoordinates[1] = y;
575         mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(),
576                 dropCoordinates);
577         return mLauncher.getWorkspace();
578     }
579 
580     /**
581      * Sets the drag listener which will be notified when a drag starts or ends.
582      */
addDragListener(DragListener l)583     public void addDragListener(DragListener l) {
584         mListeners.add(l);
585     }
586 
587     /**
588      * Remove a previously installed drag listener.
589      */
removeDragListener(DragListener l)590     public void removeDragListener(DragListener l) {
591         mListeners.remove(l);
592     }
593 
594     /**
595      * Add a DropTarget to the list of potential places to receive drop events.
596      */
addDropTarget(DropTarget target)597     public void addDropTarget(DropTarget target) {
598         mDropTargets.add(target);
599     }
600 
601     /**
602      * Don't send drop events to <em>target</em> any more.
603      */
removeDropTarget(DropTarget target)604     public void removeDropTarget(DropTarget target) {
605         mDropTargets.remove(target);
606     }
607 }
608