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