1 /*
2  * Copyright (C) 2018 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 package com.android.quickstep;
17 
18 import static android.view.MotionEvent.ACTION_CANCEL;
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_UP;
21 
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.view.InputEvent;
25 import android.view.KeyEvent;
26 import android.view.MotionEvent;
27 
28 import androidx.annotation.UiThread;
29 
30 import com.android.launcher3.util.Preconditions;
31 import com.android.quickstep.inputconsumers.InputConsumer;
32 import com.android.quickstep.util.SwipeAnimationTargetSet;
33 import com.android.systemui.shared.system.InputConsumerController;
34 
35 import java.util.ArrayList;
36 import java.util.function.Supplier;
37 
38 /**
39  * Wrapper around RecentsAnimationController to help with some synchronization
40  */
41 public class RecentsAnimationWrapper {
42 
43     private static final String TAG = "RecentsAnimationWrapper";
44 
45     // A list of callbacks to run when we receive the recents animation target. There are different
46     // than the state callbacks as these run on the current worker thread.
47     private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
48 
49     public SwipeAnimationTargetSet targetSet;
50 
51     private boolean mWindowThresholdCrossed = false;
52 
53     private final InputConsumerController mInputConsumerController;
54     private final Supplier<InputConsumer> mInputProxySupplier;
55 
56     private InputConsumer mInputConsumer;
57     private boolean mTouchInProgress;
58 
59     private boolean mFinishPending;
60 
RecentsAnimationWrapper(InputConsumerController inputConsumerController, Supplier<InputConsumer> inputProxySupplier)61     public RecentsAnimationWrapper(InputConsumerController inputConsumerController,
62             Supplier<InputConsumer> inputProxySupplier) {
63         mInputConsumerController = inputConsumerController;
64         mInputProxySupplier = inputProxySupplier;
65     }
66 
hasTargets()67     public boolean hasTargets() {
68         return targetSet != null && targetSet.hasTargets();
69     }
70 
71     @UiThread
setController(SwipeAnimationTargetSet targetSet)72     public synchronized void setController(SwipeAnimationTargetSet targetSet) {
73         Preconditions.assertUIThread();
74         this.targetSet = targetSet;
75 
76         if (targetSet == null) {
77             return;
78         }
79         targetSet.setWindowThresholdCrossed(mWindowThresholdCrossed);
80 
81         if (!mCallbacks.isEmpty()) {
82             for (Runnable action : new ArrayList<>(mCallbacks)) {
83                 action.run();
84             }
85             mCallbacks.clear();
86         }
87     }
88 
runOnInit(Runnable action)89     public synchronized void runOnInit(Runnable action) {
90         if (targetSet == null) {
91             mCallbacks.add(action);
92         } else {
93             action.run();
94         }
95     }
96 
97     /** See {@link #finish(boolean, Runnable, boolean)} */
98     @UiThread
finish(boolean toRecents, Runnable onFinishComplete)99     public void finish(boolean toRecents, Runnable onFinishComplete) {
100         finish(toRecents, onFinishComplete, false /* sendUserLeaveHint */);
101     }
102 
103     /**
104      * @param onFinishComplete A callback that runs on the main thread after the animation
105      *                         controller has finished on the background thread.
106      * @param sendUserLeaveHint Determines whether userLeaveHint flag will be set on the pausing
107      *                          activity. If userLeaveHint is true, the activity will enter into
108      *                          picture-in-picture mode upon being paused.
109      */
110     @UiThread
finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint)111     public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
112         Preconditions.assertUIThread();
113         if (!toRecents) {
114             finishAndClear(false, onFinishComplete, sendUserLeaveHint);
115         } else {
116             if (mTouchInProgress) {
117                 mFinishPending = true;
118                 // Execute the callback
119                 if (onFinishComplete != null) {
120                     onFinishComplete.run();
121                 }
122             } else {
123                 finishAndClear(true, onFinishComplete, sendUserLeaveHint);
124             }
125         }
126     }
127 
finishAndClear(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint)128     private void finishAndClear(boolean toRecents, Runnable onFinishComplete,
129             boolean sendUserLeaveHint) {
130         SwipeAnimationTargetSet controller = targetSet;
131         targetSet = null;
132         disableInputProxy();
133         if (controller != null) {
134             controller.finishController(toRecents, onFinishComplete, sendUserLeaveHint);
135         }
136     }
137 
enableInputConsumer()138     public void enableInputConsumer() {
139         if (targetSet != null) {
140             targetSet.enableInputConsumer();
141         }
142     }
143 
144     /**
145      * Indicates that the gesture has crossed the window boundary threshold and system UI can be
146      * update the represent the window behind
147      */
setWindowThresholdCrossed(boolean windowThresholdCrossed)148     public void setWindowThresholdCrossed(boolean windowThresholdCrossed) {
149         if (mWindowThresholdCrossed != windowThresholdCrossed) {
150             mWindowThresholdCrossed = windowThresholdCrossed;
151             if (targetSet != null) {
152                 targetSet.setWindowThresholdCrossed(windowThresholdCrossed);
153             }
154         }
155     }
156 
enableInputProxy()157     public void enableInputProxy() {
158         mInputConsumerController.setInputListener(this::onInputConsumerEvent);
159     }
160 
disableInputProxy()161     private void disableInputProxy() {
162         if (mInputConsumer != null && mTouchInProgress) {
163             long now = SystemClock.uptimeMillis();
164             MotionEvent dummyCancel = MotionEvent.obtain(now,  now, ACTION_CANCEL, 0, 0, 0);
165             mInputConsumer.onMotionEvent(dummyCancel);
166             dummyCancel.recycle();
167         }
168         mInputConsumerController.setInputListener(null);
169     }
170 
onInputConsumerEvent(InputEvent ev)171     private boolean onInputConsumerEvent(InputEvent ev) {
172         if (ev instanceof MotionEvent) {
173             onInputConsumerMotionEvent((MotionEvent) ev);
174         } else if (ev instanceof KeyEvent) {
175             if (mInputConsumer == null) {
176                 mInputConsumer = mInputProxySupplier.get();
177             }
178             mInputConsumer.onKeyEvent((KeyEvent) ev);
179             return true;
180         }
181         return false;
182     }
183 
onInputConsumerMotionEvent(MotionEvent ev)184     private boolean onInputConsumerMotionEvent(MotionEvent ev) {
185         int action = ev.getAction();
186 
187         // Just to be safe, verify that ACTION_DOWN comes before any other action,
188         // and ignore any ACTION_DOWN after the first one (though that should not happen).
189         if (!mTouchInProgress && action != ACTION_DOWN) {
190             Log.w(TAG, "Received non-down motion before down motion: " + action);
191             return false;
192         }
193         if (mTouchInProgress && action == ACTION_DOWN) {
194             Log.w(TAG, "Received down motion while touch was already in progress");
195             return false;
196         }
197 
198         if (action == ACTION_DOWN) {
199             mTouchInProgress = true;
200             if (mInputConsumer == null) {
201                 mInputConsumer = mInputProxySupplier.get();
202             }
203         } else if (action == ACTION_CANCEL || action == ACTION_UP) {
204             // Finish any pending actions
205             mTouchInProgress = false;
206             if (mFinishPending) {
207                 mFinishPending = false;
208                 finishAndClear(true /* toRecents */, null, false /* sendUserLeaveHint */);
209             }
210         }
211         if (mInputConsumer != null) {
212             mInputConsumer.onMotionEvent(ev);
213         }
214 
215         return true;
216     }
217 
setDeferCancelUntilNextTransition(boolean defer, boolean screenshot)218     public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
219         if (targetSet != null) {
220             targetSet.controller.setDeferCancelUntilNextTransition(defer, screenshot);
221         }
222     }
223 
getController()224     public SwipeAnimationTargetSet getController() {
225         return targetSet;
226     }
227 }
228