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