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