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