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