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