1 /*
2  * Copyright (C) 2014 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.systemui.recents.views;
18 
19 import android.app.ActivityTaskManager;
20 import android.graphics.Point;
21 import android.graphics.Rect;
22 import android.view.InputDevice;
23 import android.view.MotionEvent;
24 import android.view.PointerIcon;
25 import android.view.View;
26 import android.view.ViewConfiguration;
27 import android.view.ViewDebug;
28 
29 import com.android.internal.policy.DividerSnapAlgorithm;
30 import com.android.systemui.recents.LegacyRecentsImpl;
31 import com.android.systemui.recents.events.EventBus;
32 import com.android.systemui.recents.events.activity.ConfigurationChangedEvent;
33 import com.android.systemui.recents.events.activity.HideRecentsEvent;
34 import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent;
35 import com.android.systemui.recents.events.ui.HideIncompatibleAppOverlayEvent;
36 import com.android.systemui.recents.events.ui.ShowIncompatibleAppOverlayEvent;
37 import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent;
38 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
39 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
40 import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent;
41 import com.android.systemui.recents.misc.SystemServicesProxy;
42 import com.android.systemui.shared.recents.model.Task;
43 
44 import java.util.ArrayList;
45 
46 /**
47  * Handles touch events for a RecentsView.
48  */
49 public class RecentsViewTouchHandler {
50 
51     private RecentsView mRv;
52 
53     @ViewDebug.ExportedProperty(deepExport=true, prefix="drag_task")
54     private Task mDragTask;
55     @ViewDebug.ExportedProperty(deepExport=true, prefix="drag_task_view_")
56     private TaskView mTaskView;
57 
58     @ViewDebug.ExportedProperty(category="recents")
59     private Point mTaskViewOffset = new Point();
60     @ViewDebug.ExportedProperty(category="recents")
61     private Point mDownPos = new Point();
62     @ViewDebug.ExportedProperty(category="recents")
63     private boolean mDragRequested;
64     @ViewDebug.ExportedProperty(category="recents")
65     private boolean mIsDragging;
66     private float mDragSlop;
67     private int mDeviceId = -1;
68 
69     private DropTarget mLastDropTarget;
70     private DividerSnapAlgorithm mDividerSnapAlgorithm;
71     private ArrayList<DropTarget> mDropTargets = new ArrayList<>();
72     private ArrayList<DockState> mVisibleDockStates = new ArrayList<>();
73 
RecentsViewTouchHandler(RecentsView rv)74     public RecentsViewTouchHandler(RecentsView rv) {
75         mRv = rv;
76         mDragSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop();
77         updateSnapAlgorithm();
78     }
79 
updateSnapAlgorithm()80     private void updateSnapAlgorithm() {
81         Rect insets = new Rect();
82         SystemServicesProxy.getInstance(mRv.getContext()).getStableInsets(insets);
83         mDividerSnapAlgorithm = DividerSnapAlgorithm.create(mRv.getContext(), insets);
84     }
85 
86     /**
87      * Registers a new drop target for the current drag only.
88      */
registerDropTargetForCurrentDrag(DropTarget target)89     public void registerDropTargetForCurrentDrag(DropTarget target) {
90         mDropTargets.add(target);
91     }
92 
93     /**
94      * Returns the set of visible dock states for this current drag.
95      */
getVisibleDockStates()96     public ArrayList<DockState> getVisibleDockStates() {
97         return mVisibleDockStates;
98     }
99 
100     /** Touch preprocessing for handling below */
onInterceptTouchEvent(MotionEvent ev)101     public boolean onInterceptTouchEvent(MotionEvent ev) {
102         return handleTouchEvent(ev) || mDragRequested;
103     }
104 
105     /** Handles touch events once we have intercepted them */
onTouchEvent(MotionEvent ev)106     public boolean onTouchEvent(MotionEvent ev) {
107         handleTouchEvent(ev);
108         if (ev.getAction() == MotionEvent.ACTION_UP && mRv.getStack().getTaskCount() == 0) {
109             EventBus.getDefault().send(new HideRecentsEvent(false, true));
110         }
111         return true;
112     }
113 
114     /**** Events ****/
115 
onBusEvent(DragStartEvent event)116     public final void onBusEvent(DragStartEvent event) {
117         SystemServicesProxy ssp = LegacyRecentsImpl.getSystemServices();
118         mRv.getParent().requestDisallowInterceptTouchEvent(true);
119         mDragRequested = true;
120         // We defer starting the actual drag handling until the user moves past the drag slop
121         mIsDragging = false;
122         mDragTask = event.task;
123         mTaskView = event.taskView;
124         mDropTargets.clear();
125 
126         int[] recentsViewLocation = new int[2];
127         mRv.getLocationInWindow(recentsViewLocation);
128         mTaskViewOffset.set(mTaskView.getLeft() - recentsViewLocation[0] + event.tlOffset.x,
129                 mTaskView.getTop() - recentsViewLocation[1] + event.tlOffset.y);
130 
131         // Change space coordinates relative to the view to RecentsView when user initiates a touch
132         if (event.isUserTouchInitiated) {
133             float x = mDownPos.x - mTaskViewOffset.x;
134             float y = mDownPos.y - mTaskViewOffset.y;
135             mTaskView.setTranslationX(x);
136             mTaskView.setTranslationY(y);
137         }
138 
139         mVisibleDockStates.clear();
140         if (ActivityTaskManager.supportsMultiWindow(mRv.getContext()) && !ssp.hasDockedTask()
141                 && mDividerSnapAlgorithm.isSplitScreenFeasible()) {
142             LegacyRecentsImpl.logDockAttempt(mRv.getContext(), event.task.getTopComponent(),
143                     event.task.resizeMode);
144             if (!event.task.isDockable) {
145                 EventBus.getDefault().send(new ShowIncompatibleAppOverlayEvent());
146             } else {
147                 // Add the dock state drop targets (these take priority)
148                 DockState[] dockStates = LegacyRecentsImpl.getConfiguration()
149                         .getDockStatesForCurrentOrientation();
150                 for (DockState dockState : dockStates) {
151                     registerDropTargetForCurrentDrag(dockState);
152                     dockState.update(mRv.getContext());
153                     mVisibleDockStates.add(dockState);
154                 }
155             }
156         }
157 
158         // Request other drop targets to register themselves
159         EventBus.getDefault().send(new DragStartInitializeDropTargetsEvent(event.task,
160                 event.taskView, this));
161         if (mDeviceId != -1) {
162             InputDevice device = InputDevice.getDevice(mDeviceId);
163             if (device != null) {
164                 device.setPointerType(PointerIcon.TYPE_GRABBING);
165             }
166         }
167     }
168 
onBusEvent(DragEndEvent event)169     public final void onBusEvent(DragEndEvent event) {
170         if (!mDragTask.isDockable) {
171             EventBus.getDefault().send(new HideIncompatibleAppOverlayEvent());
172         }
173         mDragRequested = false;
174         mDragTask = null;
175         mTaskView = null;
176         mLastDropTarget = null;
177     }
178 
onBusEvent(ConfigurationChangedEvent event)179     public final void onBusEvent(ConfigurationChangedEvent event) {
180         if (event.fromDisplayDensityChange || event.fromDeviceOrientationChange) {
181             updateSnapAlgorithm();
182         }
183     }
184 
cancelStackActionButtonClick()185     void cancelStackActionButtonClick() {
186         mRv.getStackActionButton().setPressed(false);
187     }
188 
isWithinStackActionButton(float x, float y)189     private boolean isWithinStackActionButton(float x, float y) {
190         Rect rect = mRv.getStackActionButtonBoundsFromStackLayout();
191         return mRv.getStackActionButton().getVisibility() == View.VISIBLE &&
192                 mRv.getStackActionButton().pointInView(x - rect.left, y - rect.top, 0 /* slop */);
193     }
194 
changeStackActionButtonDrawableHotspot(float x, float y)195     private void changeStackActionButtonDrawableHotspot(float x, float y) {
196         Rect rect = mRv.getStackActionButtonBoundsFromStackLayout();
197         mRv.getStackActionButton().drawableHotspotChanged(x - rect.left, y - rect.top);
198     }
199 
200     /**
201      * Handles dragging touch events
202      */
handleTouchEvent(MotionEvent ev)203     private boolean handleTouchEvent(MotionEvent ev) {
204         int action = ev.getActionMasked();
205         boolean consumed = false;
206         float evX = ev.getX();
207         float evY = ev.getY();
208         switch (action) {
209             case MotionEvent.ACTION_DOWN:
210                 mDownPos.set((int) evX, (int) evY);
211                 mDeviceId = ev.getDeviceId();
212 
213                 if (isWithinStackActionButton(evX, evY)) {
214                     changeStackActionButtonDrawableHotspot(evX, evY);
215                     mRv.getStackActionButton().setPressed(true);
216                 }
217                 break;
218             case MotionEvent.ACTION_MOVE: {
219                 float x = evX - mTaskViewOffset.x;
220                 float y = evY - mTaskViewOffset.y;
221 
222                 if (mRv.getStackActionButton().isPressed() && isWithinStackActionButton(evX, evY)) {
223                     changeStackActionButtonDrawableHotspot(evX, evY);
224                 }
225 
226                 if (mDragRequested) {
227                     if (!mIsDragging) {
228                         mIsDragging = Math.hypot(evX - mDownPos.x, evY - mDownPos.y) > mDragSlop;
229                     }
230                     if (mIsDragging) {
231                         int width = mRv.getMeasuredWidth();
232                         int height = mRv.getMeasuredHeight();
233 
234                         DropTarget currentDropTarget = null;
235 
236                         // Give priority to the current drop target to retain the touch handling
237                         if (mLastDropTarget != null) {
238                             if (mLastDropTarget.acceptsDrop((int) evX, (int) evY, width, height,
239                                     mRv.mSystemInsets, true /* isCurrentTarget */)) {
240                                 currentDropTarget = mLastDropTarget;
241                             }
242                         }
243 
244                         // Otherwise, find the next target to handle this event
245                         if (currentDropTarget == null) {
246                             for (DropTarget target : mDropTargets) {
247                                 if (target.acceptsDrop((int) evX, (int) evY, width, height,
248                                         mRv.mSystemInsets, false /* isCurrentTarget */)) {
249                                     currentDropTarget = target;
250                                     break;
251                                 }
252                             }
253                         }
254                         if (mLastDropTarget != currentDropTarget) {
255                             mLastDropTarget = currentDropTarget;
256                             EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask,
257                                     currentDropTarget));
258                         }
259                     }
260                     mTaskView.setTranslationX(x);
261                     mTaskView.setTranslationY(y);
262                 }
263                 break;
264             }
265             case MotionEvent.ACTION_UP:
266             case MotionEvent.ACTION_CANCEL: {
267                 if (mRv.getStackActionButton().isPressed() && isWithinStackActionButton(evX, evY)) {
268                     EventBus.getDefault().send(new DismissAllTaskViewsEvent());
269                     consumed = true;
270                 }
271                 cancelStackActionButtonClick();
272                 if (mDragRequested) {
273                     boolean cancelled = action == MotionEvent.ACTION_CANCEL;
274                     if (cancelled) {
275                         EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask, null));
276                     }
277                     EventBus.getDefault().send(new DragEndEvent(mDragTask, mTaskView,
278                             !cancelled ? mLastDropTarget : null));
279                     break;
280                 }
281                 mDeviceId = -1;
282             }
283         }
284         return consumed;
285     }
286 }
287