1 /* 2 * Copyright (C) 2019 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 com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 20 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; 21 22 import android.annotation.TargetApi; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.os.Build; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 31 import androidx.annotation.CallSuper; 32 import androidx.annotation.UiThread; 33 34 import com.android.launcher3.DeviceProfile; 35 import com.android.launcher3.statemanager.StatefulActivity; 36 import com.android.launcher3.testing.TestProtocol; 37 import com.android.launcher3.util.VibratorWrapper; 38 import com.android.launcher3.util.WindowBounds; 39 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; 40 import com.android.quickstep.util.ActiveGestureLog; 41 import com.android.quickstep.util.ActivityInitListener; 42 import com.android.quickstep.util.RectFSpringAnim; 43 import com.android.quickstep.util.SurfaceTransactionApplier; 44 import com.android.quickstep.util.TransformParams; 45 import com.android.quickstep.views.RecentsView; 46 import com.android.quickstep.views.TaskView; 47 import com.android.systemui.shared.recents.model.ThumbnailData; 48 import com.android.systemui.shared.system.InputConsumerController; 49 import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 50 51 import java.util.ArrayList; 52 import java.util.function.Consumer; 53 54 /** 55 * Base class for swipe up handler with some utility methods 56 */ 57 @TargetApi(Build.VERSION_CODES.Q) 58 public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView> 59 extends SwipeUpAnimationLogic implements RecentsAnimationListener { 60 61 private static final String TAG = "BaseSwipeUpHandler"; 62 63 protected final BaseActivityInterface<?, T> mActivityInterface; 64 protected final InputConsumerController mInputConsumer; 65 66 protected final ActivityInitListener mActivityInitListener; 67 68 protected RecentsAnimationController mRecentsAnimationController; 69 protected RecentsAnimationTargets mRecentsAnimationTargets; 70 71 // Callbacks to be made once the recents animation starts 72 private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>(); 73 74 protected T mActivity; 75 protected Q mRecentsView; 76 77 protected Runnable mGestureEndCallback; 78 79 protected MultiStateCallback mStateCallback; 80 81 protected boolean mCanceled; 82 83 private boolean mRecentsViewScrollLinked = false; 84 BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState, GestureState gestureState, InputConsumerController inputConsumer)85 protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState, 86 GestureState gestureState, InputConsumerController inputConsumer) { 87 super(context, deviceState, gestureState, new TransformParams()); 88 mActivityInterface = gestureState.getActivityInterface(); 89 mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit); 90 mInputConsumer = inputConsumer; 91 } 92 93 /** 94 * To be called at the end of constructor of subclasses. This calls various methods which can 95 * depend on proper class initialization. 96 */ initAfterSubclassConstructor()97 protected void initAfterSubclassConstructor() { 98 initTransitionEndpoints( 99 mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile()); 100 } 101 performHapticFeedback()102 protected void performHapticFeedback() { 103 VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC); 104 } 105 getRecentsViewDispatcher(float navbarRotation)106 public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) { 107 return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null; 108 } 109 setGestureEndCallback(Runnable gestureEndCallback)110 public void setGestureEndCallback(Runnable gestureEndCallback) { 111 mGestureEndCallback = gestureEndCallback; 112 } 113 getLaunchIntent()114 public abstract Intent getLaunchIntent(); 115 linkRecentsViewScroll()116 protected void linkRecentsViewScroll() { 117 SurfaceTransactionApplier.create(mRecentsView, applier -> { 118 mTransformParams.setSyncTransactionApplier(applier); 119 runOnRecentsAnimationStart(() -> 120 mRecentsAnimationTargets.addReleaseCheck(applier)); 121 }); 122 123 mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { 124 if (moveWindowWithRecentsScroll()) { 125 updateFinalShift(); 126 } 127 }); 128 runOnRecentsAnimationStart(() -> 129 mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController, 130 mRecentsAnimationTargets)); 131 mRecentsViewScrollLinked = true; 132 } 133 startNewTask(Consumer<Boolean> resultCallback)134 protected void startNewTask(Consumer<Boolean> resultCallback) { 135 // Launch the task user scrolled to (mRecentsView.getNextPage()). 136 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 137 // We finish recents animation inside launchTask() when live tile is enabled. 138 mRecentsView.getNextPageTaskView().launchTask(false /* animate */, 139 true /* freezeTaskList */); 140 } else { 141 int taskId = mRecentsView.getNextPageTaskView().getTask().key.id; 142 if (!mCanceled) { 143 TaskView nextTask = mRecentsView.getTaskView(taskId); 144 if (nextTask != null) { 145 mGestureState.updateLastStartedTaskId(taskId); 146 boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds() 147 .contains(taskId); 148 nextTask.launchTask(false /* animate */, true /* freezeTaskList */, 149 success -> { 150 resultCallback.accept(success); 151 if (success) { 152 if (hasTaskPreviouslyAppeared) { 153 onRestartPreviouslyAppearedTask(); 154 } 155 } else { 156 mActivityInterface.onLaunchTaskFailed(); 157 nextTask.notifyTaskLaunchFailed(TAG); 158 mRecentsAnimationController.finish(true /* toRecents */, null); 159 } 160 }, MAIN_EXECUTOR.getHandler()); 161 } 162 } 163 mCanceled = false; 164 } 165 } 166 167 /** 168 * Called when we successfully startNewTask() on the task that was previously running. Normally 169 * we call resumeLastTask() when returning to the previously running task, but this handles a 170 * specific edge case: if we switch from A to B, and back to A before B appears, we need to 171 * start A again to ensure it stays on top. 172 */ 173 @CallSuper onRestartPreviouslyAppearedTask()174 protected void onRestartPreviouslyAppearedTask() { 175 // Finish the controller here, since we won't get onTaskAppeared() for a task that already 176 // appeared. 177 if (mRecentsAnimationController != null) { 178 mRecentsAnimationController.finish(false, null); 179 } 180 } 181 182 /** 183 * Runs the given {@param action} if the recents animation has already started, or queues it to 184 * be run when it is next started. 185 */ runOnRecentsAnimationStart(Runnable action)186 protected void runOnRecentsAnimationStart(Runnable action) { 187 if (mRecentsAnimationTargets == null) { 188 mRecentsAnimationStartCallbacks.add(action); 189 } else { 190 action.run(); 191 } 192 } 193 194 /** 195 * TODO can we remove this now that we don't finish the controller until onTaskAppeared()? 196 * @return whether the recents animation has started and there are valid app targets. 197 */ hasTargets()198 protected boolean hasTargets() { 199 return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets(); 200 } 201 202 @Override onRecentsAnimationStart(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets targets)203 public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController, 204 RecentsAnimationTargets targets) { 205 mRecentsAnimationController = recentsAnimationController; 206 mRecentsAnimationTargets = targets; 207 mTransformParams.setTargetSet(mRecentsAnimationTargets); 208 RemoteAnimationTargetCompat runningTaskTarget = targets.findTask( 209 mGestureState.getRunningTaskId()); 210 211 if (runningTaskTarget != null) { 212 mTaskViewSimulator.setPreview(runningTaskTarget); 213 } 214 215 // Only initialize the device profile, if it has not been initialized before, as in some 216 // configurations targets.homeContentInsets may not be correct. 217 if (mActivity == null) { 218 DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile(); 219 if (targets.minimizedHomeBounds != null && runningTaskTarget != null) { 220 Rect overviewStackBounds = mActivityInterface 221 .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget); 222 dp = dp.getMultiWindowProfile(mContext, 223 new WindowBounds(overviewStackBounds, targets.homeContentInsets)); 224 } else { 225 // If we are not in multi-window mode, home insets should be same as system insets. 226 dp = dp.copy(mContext); 227 } 228 dp.updateInsets(targets.homeContentInsets); 229 dp.updateIsSeascape(mContext); 230 initTransitionEndpoints(dp); 231 } 232 233 // Notify when the animation starts 234 if (!mRecentsAnimationStartCallbacks.isEmpty()) { 235 for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) { 236 action.run(); 237 } 238 mRecentsAnimationStartCallbacks.clear(); 239 } 240 } 241 242 @Override onRecentsAnimationCanceled(ThumbnailData thumbnailData)243 public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) { 244 mRecentsAnimationController = null; 245 mRecentsAnimationTargets = null; 246 if (mRecentsView != null) { 247 mRecentsView.setRecentsAnimationTargets(null, null); 248 } 249 } 250 251 @Override onRecentsAnimationFinished(RecentsAnimationController controller)252 public void onRecentsAnimationFinished(RecentsAnimationController controller) { 253 mRecentsAnimationController = null; 254 mRecentsAnimationTargets = null; 255 if (mRecentsView != null) { 256 mRecentsView.setRecentsAnimationTargets(null, null); 257 } 258 } 259 260 @Override onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget)261 public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) { 262 if (mRecentsAnimationController != null) { 263 if (handleTaskAppeared(appearedTaskTarget)) { 264 mRecentsAnimationController.finish(false /* toRecents */, 265 null /* onFinishComplete */); 266 mActivityInterface.onLaunchTaskSuccess(); 267 ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false); 268 } 269 } 270 } 271 272 /** @return Whether this was the task we were waiting to appear, and thus handled it. */ handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget)273 protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget); 274 275 /** 276 * @return The index of the TaskView in RecentsView whose taskId matches the task that will 277 * resume if we finish the controller. 278 */ getLastAppearedTaskIndex()279 protected int getLastAppearedTaskIndex() { 280 return mGestureState.getLastAppearedTaskId() != -1 281 ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId()) 282 : mRecentsView.getRunningTaskIndex(); 283 } 284 285 /** 286 * @return Whether we are continuing a gesture that already landed on a new task, 287 * but before that task appeared. 288 */ hasStartedNewTask()289 protected boolean hasStartedNewTask() { 290 return mGestureState.getLastStartedTaskId() != -1; 291 } 292 293 /** 294 * Return true if the window should be translated horizontally if the recents view scrolls 295 */ moveWindowWithRecentsScroll()296 protected abstract boolean moveWindowWithRecentsScroll(); 297 onActivityInit(Boolean alreadyOnHome)298 protected boolean onActivityInit(Boolean alreadyOnHome) { 299 T createdActivity = mActivityInterface.getCreatedActivity(); 300 if (TestProtocol.sDebugTracing) { 301 Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1"); 302 } 303 if (createdActivity != null) { 304 if (TestProtocol.sDebugTracing) { 305 Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2"); 306 } 307 initTransitionEndpoints(createdActivity.getDeviceProfile()); 308 } 309 return true; 310 } 311 312 /** 313 * Called to create a input proxy for the running task 314 */ 315 @UiThread createNewInputProxyHandler()316 protected abstract InputConsumer createNewInputProxyHandler(); 317 318 /** 319 * Called when the value of {@link #mCurrentShift} changes 320 */ 321 @UiThread updateFinalShift()322 public abstract void updateFinalShift(); 323 324 /** 325 * Called when motion pause is detected 326 */ onMotionPauseChanged(boolean isPaused)327 public abstract void onMotionPauseChanged(boolean isPaused); 328 329 @UiThread onGestureStarted(boolean isLikelyToStartNewTask)330 public void onGestureStarted(boolean isLikelyToStartNewTask) { } 331 332 @UiThread onGestureCancelled()333 public abstract void onGestureCancelled(); 334 335 @UiThread onGestureEnded(float endVelocity, PointF velocity, PointF downPos)336 public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos); 337 onConsumerAboutToBeSwitched()338 public abstract void onConsumerAboutToBeSwitched(); 339 setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask)340 public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { } 341 342 /** 343 * Registers a callback to run when the activity is ready. 344 * @param intent The intent that will be used to start the activity if it doesn't exist already. 345 */ initWhenReady(Intent intent)346 public void initWhenReady(Intent intent) { 347 // Preload the plan 348 RecentsModel.INSTANCE.get(mContext).getTasks(null); 349 350 mActivityInitListener.register(intent); 351 } 352 353 /** 354 * Applies the transform on the recents animation 355 */ applyWindowTransform()356 protected void applyWindowTransform() { 357 if (mWindowTransitionController != null) { 358 float progress = mCurrentShift.value / mDragLengthFactor; 359 mWindowTransitionController.setPlayFraction(progress); 360 } 361 if (mRecentsAnimationTargets != null) { 362 if (mRecentsViewScrollLinked) { 363 mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset()); 364 } 365 mTaskViewSimulator.apply(mTransformParams); 366 } 367 } 368 369 @Override createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)370 protected RectFSpringAnim createWindowAnimationToHome(float startProgress, 371 HomeAnimationFactory homeAnimationFactory) { 372 RectFSpringAnim anim = 373 super.createWindowAnimationToHome(startProgress, homeAnimationFactory); 374 if (mRecentsAnimationTargets != null) { 375 mRecentsAnimationTargets.addReleaseCheck(anim); 376 } 377 return anim; 378 } 379 380 public interface Factory { 381 newHandler( GestureState gestureState, long touchTimeMs, boolean continuingLastGesture)382 BaseSwipeUpHandler newHandler( 383 GestureState gestureState, long touchTimeMs, boolean continuingLastGesture); 384 } 385 } 386