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 static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
23 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
24 
25 import android.os.SystemClock;
26 import android.util.Log;
27 import android.view.InputEvent;
28 import android.view.KeyEvent;
29 import android.view.MotionEvent;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.UiThread;
33 
34 import com.android.launcher3.util.Preconditions;
35 import com.android.systemui.shared.recents.model.ThumbnailData;
36 import com.android.systemui.shared.system.InputConsumerController;
37 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
38 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
39 
40 import java.util.function.Consumer;
41 import java.util.function.Supplier;
42 
43 /**
44  * Wrapper around RecentsAnimationControllerCompat to help with some synchronization
45  */
46 public class RecentsAnimationController {
47 
48     private static final String TAG = "RecentsAnimationController";
49 
50     private final RecentsAnimationControllerCompat mController;
51     private final Consumer<RecentsAnimationController> mOnFinishedListener;
52     private final boolean mAllowMinimizeSplitScreen;
53 
54     private InputConsumerController mInputConsumerController;
55     private Supplier<InputConsumer> mInputProxySupplier;
56     private InputConsumer mInputConsumer;
57     private boolean mUseLauncherSysBarFlags = false;
58     private boolean mSplitScreenMinimized = false;
59     private boolean mTouchInProgress;
60     private boolean mDisableInputProxyPending;
61 
RecentsAnimationController(RecentsAnimationControllerCompat controller, boolean allowMinimizeSplitScreen, Consumer<RecentsAnimationController> onFinishedListener)62     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
63             boolean allowMinimizeSplitScreen,
64             Consumer<RecentsAnimationController> onFinishedListener) {
65         mController = controller;
66         mOnFinishedListener = onFinishedListener;
67         mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
68     }
69 
70     /**
71      * Synchronously takes a screenshot of the task with the given {@param taskId} if the task is
72      * currently being animated.
73      */
screenshotTask(int taskId)74     public ThumbnailData screenshotTask(int taskId) {
75         return mController.screenshotTask(taskId);
76     }
77 
78     /**
79      * Indicates that the gesture has crossed the window boundary threshold and system UI can be
80      * update the system bar flags accordingly.
81      */
setUseLauncherSystemBarFlags(boolean useLauncherSysBarFlags)82     public void setUseLauncherSystemBarFlags(boolean useLauncherSysBarFlags) {
83         if (mUseLauncherSysBarFlags != useLauncherSysBarFlags) {
84             mUseLauncherSysBarFlags = useLauncherSysBarFlags;
85             UI_HELPER_EXECUTOR.execute(() -> {
86                 mController.setAnimationTargetsBehindSystemBars(!useLauncherSysBarFlags);
87             });
88         }
89     }
90 
91     /**
92      * Indicates that the gesture has crossed the window boundary threshold and we should minimize
93      * if we are in splitscreen.
94      */
setSplitScreenMinimized(boolean splitScreenMinimized)95     public void setSplitScreenMinimized(boolean splitScreenMinimized) {
96         if (!mAllowMinimizeSplitScreen) {
97             return;
98         }
99         if (mSplitScreenMinimized != splitScreenMinimized) {
100             mSplitScreenMinimized = splitScreenMinimized;
101             UI_HELPER_EXECUTOR.execute(() -> {
102                 SystemUiProxy p = SystemUiProxy.INSTANCE.getNoCreate();
103                 if (p != null) {
104                     p.setSplitScreenMinimized(splitScreenMinimized);
105                 }
106             });
107         }
108     }
109 
110     /**
111      * Notifies the controller that we want to defer cancel until the next app transition starts.
112      * If {@param screenshot} is set, then we will receive a screenshot on the next
113      * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)} and we must also call
114      * {@link #cleanupScreenshot()} when that screenshot is no longer used.
115      */
setDeferCancelUntilNextTransition(boolean defer, boolean screenshot)116     public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
117         mController.setDeferCancelUntilNextTransition(defer, screenshot);
118     }
119 
120     /**
121      * Cleans up the screenshot previously returned from
122      * {@link RecentsAnimationCallbacks#onAnimationCanceled(ThumbnailData)}.
123      */
cleanupScreenshot()124     public void cleanupScreenshot() {
125         UI_HELPER_EXECUTOR.execute(() -> mController.cleanupScreenshot());
126     }
127 
128     /**
129      * Remove task remote animation target from
130      * {@link RecentsAnimationCallbacks#onTaskAppeared(RemoteAnimationTargetCompat)}}.
131      */
132     @UiThread
removeTaskTarget(@onNull RemoteAnimationTargetCompat target)133     public boolean removeTaskTarget(@NonNull RemoteAnimationTargetCompat target) {
134         return mController.removeTask(target.taskId);
135     }
136 
137     @UiThread
finishAnimationToHome()138     public void finishAnimationToHome() {
139         finishAndDisableInputProxy(true /* toRecents */, null, false /* sendUserLeaveHint */);
140     }
141 
142     @UiThread
finishAnimationToApp()143     public void finishAnimationToApp() {
144         finishAndDisableInputProxy(false /* toRecents */, null, false /* sendUserLeaveHint */);
145     }
146 
147     /** See {@link #finish(boolean, Runnable, boolean)} */
148     @UiThread
finish(boolean toRecents, Runnable onFinishComplete)149     public void finish(boolean toRecents, Runnable onFinishComplete) {
150         finish(toRecents, onFinishComplete, false /* sendUserLeaveHint */);
151     }
152 
153     /**
154      * @param onFinishComplete A callback that runs on the main thread after the animation
155      *                         controller has finished on the background thread.
156      * @param sendUserLeaveHint Determines whether userLeaveHint flag will be set on the pausing
157      *                          activity. If userLeaveHint is true, the activity will enter into
158      *                          picture-in-picture mode upon being paused.
159      */
160     @UiThread
finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint)161     public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
162         Preconditions.assertUIThread();
163         if (toRecents && mTouchInProgress) {
164             // Finish the controller as requested, but don't disable input proxy yet.
165             mDisableInputProxyPending = true;
166             finishController(toRecents, onFinishComplete, sendUserLeaveHint);
167         } else {
168             finishAndDisableInputProxy(toRecents, onFinishComplete, sendUserLeaveHint);
169         }
170     }
171 
finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint)172     private void finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete,
173             boolean sendUserLeaveHint) {
174         disableInputProxy();
175         finishController(toRecents, onFinishComplete, sendUserLeaveHint);
176     }
177 
178     @UiThread
finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint)179     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
180         mOnFinishedListener.accept(this);
181         UI_HELPER_EXECUTOR.execute(() -> {
182             mController.setInputConsumerEnabled(false);
183             mController.finish(toRecents, sendUserLeaveHint);
184             if (callback != null) {
185                 MAIN_EXECUTOR.execute(callback);
186             }
187         });
188     }
189 
190     /**
191      * Enables the input consumer to start intercepting touches in the app window.
192      */
enableInputConsumer()193     public void enableInputConsumer() {
194         UI_HELPER_EXECUTOR.submit(() -> {
195             mController.hideCurrentInputMethod();
196             mController.setInputConsumerEnabled(true);
197         });
198     }
199 
enableInputProxy(InputConsumerController inputConsumerController, Supplier<InputConsumer> inputProxySupplier)200     public void enableInputProxy(InputConsumerController inputConsumerController,
201             Supplier<InputConsumer> inputProxySupplier) {
202         mInputProxySupplier = inputProxySupplier;
203         mInputConsumerController = inputConsumerController;
204         mInputConsumerController.setInputListener(this::onInputConsumerEvent);
205     }
206 
207     /** @return wrapper controller. */
getController()208     public RecentsAnimationControllerCompat getController() {
209         return mController;
210     }
211 
disableInputProxy()212     private void disableInputProxy() {
213         if (mInputConsumer != null && mTouchInProgress) {
214             long now = SystemClock.uptimeMillis();
215             MotionEvent dummyCancel = MotionEvent.obtain(now,  now, ACTION_CANCEL, 0, 0, 0);
216             mInputConsumer.onMotionEvent(dummyCancel);
217             dummyCancel.recycle();
218         }
219         if (mInputConsumerController != null) {
220             mInputConsumerController.setInputListener(null);
221         }
222         mInputProxySupplier = null;
223     }
224 
onInputConsumerEvent(InputEvent ev)225     private boolean onInputConsumerEvent(InputEvent ev) {
226         if (ev instanceof MotionEvent) {
227             onInputConsumerMotionEvent((MotionEvent) ev);
228         } else if (ev instanceof KeyEvent) {
229             if (mInputConsumer == null) {
230                 mInputConsumer = mInputProxySupplier.get();
231             }
232             mInputConsumer.onKeyEvent((KeyEvent) ev);
233             return true;
234         }
235         return false;
236     }
237 
onInputConsumerMotionEvent(MotionEvent ev)238     private boolean onInputConsumerMotionEvent(MotionEvent ev) {
239         int action = ev.getAction();
240 
241         // Just to be safe, verify that ACTION_DOWN comes before any other action,
242         // and ignore any ACTION_DOWN after the first one (though that should not happen).
243         if (!mTouchInProgress && action != ACTION_DOWN) {
244             Log.w(TAG, "Received non-down motion before down motion: " + action);
245             return false;
246         }
247         if (mTouchInProgress && action == ACTION_DOWN) {
248             Log.w(TAG, "Received down motion while touch was already in progress");
249             return false;
250         }
251 
252         if (action == ACTION_DOWN) {
253             mTouchInProgress = true;
254             if (mInputConsumer == null) {
255                 mInputConsumer = mInputProxySupplier.get();
256             }
257         } else if (action == ACTION_CANCEL || action == ACTION_UP) {
258             // Finish any pending actions
259             mTouchInProgress = false;
260             if (mDisableInputProxyPending) {
261                 mDisableInputProxyPending = false;
262                 disableInputProxy();
263             }
264         }
265         if (mInputConsumer != null) {
266             mInputConsumer.onMotionEvent(ev);
267         }
268 
269         return true;
270     }
271 }
272