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 android.content.ComponentName;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.os.IBinder;
25import android.view.DragEvent;
26import android.view.HapticFeedbackConstants;
27import android.view.KeyEvent;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.inputmethod.InputMethodManager;
31
32import com.android.launcher3.DragSource;
33import com.android.launcher3.DropTarget;
34import com.android.launcher3.ItemInfo;
35import com.android.launcher3.Launcher;
36import com.android.launcher3.R;
37import com.android.launcher3.ShortcutInfo;
38import com.android.launcher3.accessibility.DragViewStateAnnouncer;
39import com.android.launcher3.util.ItemInfoMatcher;
40import com.android.launcher3.util.Thunk;
41import com.android.launcher3.util.TouchController;
42
43import java.util.ArrayList;
44
45/**
46 * Class for initiating a drag within a view or across multiple views.
47 */
48public 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         */
105        void onDragStart(DropTarget.DragObject dragObject, DragOptions options);
106
107        /**
108         * The drag has ended
109         */
110        void onDragEnd();
111    }
112
113    /**
114     * Used to create a new DragLayer from XML.
115     */
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     */
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        dragView.setItemInfo(dragInfo);
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
212    private void callOnDragStart() {
213        if (mOptions.preDragCondition != null) {
214            mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/);
215        }
216        mIsInPreDrag = false;
217        for (DragListener listener : new ArrayList<>(mListeners)) {
218            listener.onDragStart(mDragObject, mOptions);
219        }
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     */
232    public boolean dispatchKeyEvent(KeyEvent event) {
233        return mDragDriver != null;
234    }
235
236    public boolean isDragging() {
237        return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag);
238    }
239
240    /**
241     * Stop dragging without dropping.
242     */
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
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
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
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
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     */
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     */
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
343    public long getLastGestureUpTime() {
344        if (mDragDriver != null) {
345            return System.currentTimeMillis();
346        } else {
347            return mLastTouchUpTime;
348        }
349    }
350
351    public void resetLastGestureUpTime() {
352        mLastTouchUpTime = -1;
353    }
354
355    @Override
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
363    public void onDriverDragExitWindow() {
364        if (mLastDropTarget != null) {
365            mLastDropTarget.onDragExit(mDragObject);
366            mLastDropTarget = null;
367        }
368    }
369
370    @Override
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
386    public void onDriverDragCancel() {
387        cancelDrag();
388    }
389
390    /**
391     * Call this from a drag source view.
392     */
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     */
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     */
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     */
440    public void setMoveTarget(View view) {
441        mMoveTarget = view;
442    }
443
444    public boolean dispatchUnhandledMove(View focused, int direction) {
445        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
446    }
447
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
469    public float getDistanceDragged() {
470        return mDistanceSinceScroll;
471    }
472
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
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     */
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     */
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     */
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
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
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
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     */
625    public void addDragListener(DragListener l) {
626        mListeners.add(l);
627    }
628
629    /**
630     * Remove a previously installed drag listener.
631     */
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     */
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     */
646    public void removeDropTarget(DropTarget target) {
647        mDropTargets.remove(target);
648    }
649
650}
651