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 android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 19 20 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 21 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 22 import static com.android.launcher3.util.NavigationMode.NO_BUTTON; 23 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; 24 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED; 25 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED; 26 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; 27 28 import android.app.ActivityManager; 29 import android.app.ActivityOptions; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.SystemProperties; 33 import android.util.Log; 34 import android.view.RemoteAnimationTarget; 35 36 import androidx.annotation.Nullable; 37 import androidx.annotation.UiThread; 38 39 import com.android.launcher3.Utilities; 40 import com.android.launcher3.config.FeatureFlags; 41 import com.android.launcher3.testing.shared.TestProtocol; 42 import com.android.launcher3.util.DisplayController; 43 import com.android.quickstep.TopTaskTracker.CachedTaskInfo; 44 import com.android.quickstep.util.ActiveGestureLog; 45 import com.android.quickstep.views.DesktopTaskView; 46 import com.android.quickstep.views.RecentsView; 47 import com.android.systemui.shared.recents.model.ThumbnailData; 48 import com.android.systemui.shared.system.ActivityManagerWrapper; 49 import com.android.systemui.shared.system.TaskStackChangeListener; 50 import com.android.systemui.shared.system.TaskStackChangeListeners; 51 52 import java.util.HashMap; 53 54 public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener { 55 public static final boolean ENABLE_SHELL_TRANSITIONS = 56 SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); 57 public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS 58 && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); 59 60 private RecentsAnimationController mController; 61 private RecentsAnimationCallbacks mCallbacks; 62 private RecentsAnimationTargets mTargets; 63 // Temporary until we can hook into gesture state events 64 private GestureState mLastGestureState; 65 private RemoteAnimationTarget[] mLastAppearedTaskTargets; 66 private Runnable mLiveTileCleanUpHandler; 67 private Context mCtx; 68 69 private final TaskStackChangeListener mLiveTileRestartListener = new TaskStackChangeListener() { 70 @Override 71 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 72 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 73 if (mLastGestureState == null) { 74 TaskStackChangeListeners.getInstance().unregisterTaskStackListener( 75 mLiveTileRestartListener); 76 return; 77 } 78 BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); 79 if (activityInterface.isInLiveTileMode() 80 && activityInterface.getCreatedActivity() != null) { 81 RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); 82 if (recentsView != null) { 83 recentsView.launchSideTaskInLiveTileModeForRestartedApp(task.taskId); 84 TaskStackChangeListeners.getInstance().unregisterTaskStackListener( 85 mLiveTileRestartListener); 86 } 87 } 88 } 89 }; 90 TaskAnimationManager(Context ctx)91 TaskAnimationManager(Context ctx) { 92 mCtx = ctx; 93 } 94 /** 95 * Preloads the recents animation. 96 */ preloadRecentsAnimation(Intent intent)97 public void preloadRecentsAnimation(Intent intent) { 98 // Pass null animation handler to indicate this start is for preloading 99 UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance() 100 .startRecentsActivity(intent, 0, null, null, null)); 101 } 102 103 /** 104 * Starts a new recents animation for the activity with the given {@param intent}. 105 */ 106 @UiThread startRecentsAnimation(GestureState gestureState, Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener)107 public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState, 108 Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) { 109 ActiveGestureLog.INSTANCE.addLog( 110 /* event= */ "startRecentsAnimation", 111 /* gestureEvent= */ START_RECENTS_ANIMATION); 112 // Notify if recents animation is still running 113 if (mController != null) { 114 String msg = "New recents animation started before old animation completed"; 115 if (FeatureFlags.IS_STUDIO_BUILD) { 116 throw new IllegalArgumentException(msg); 117 } else { 118 Log.e("TaskAnimationManager", msg, new Exception()); 119 } 120 } 121 // But force-finish it anyways 122 finishRunningRecentsAnimation(false /* toHome */, true /* forceFinish */); 123 124 if (mCallbacks != null) { 125 // If mCallbacks still != null, that means we are getting this startRecentsAnimation() 126 // before the previous one got onRecentsAnimationStart(). In that case, cleanup the 127 // previous animation so it doesn't mess up/listen to state changes in this animation. 128 cleanUpRecentsAnimation(mCallbacks); 129 } 130 131 final BaseActivityInterface activityInterface = gestureState.getActivityInterface(); 132 mLastGestureState = gestureState; 133 RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks( 134 SystemUiProxy.INSTANCE.get(mCtx), activityInterface.allowMinimizeSplitScreen()); 135 mCallbacks = newCallbacks; 136 mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() { 137 @Override 138 public void onRecentsAnimationStart(RecentsAnimationController controller, 139 RecentsAnimationTargets targets) { 140 if (mCallbacks == null) { 141 // It's possible for the recents animation to have finished and be cleaned up 142 // by the time we process the start callback, and in that case, just we can skip 143 // handling this call entirely 144 return; 145 } 146 mController = controller; 147 mTargets = targets; 148 // TODO(b/236226779): We can probably get away w/ setting mLastAppearedTaskTargets 149 // to all appeared targets directly vs just looking at running ones 150 int[] runningTaskIds = mLastGestureState.getRunningTaskIds(targets.apps.length > 1); 151 mLastAppearedTaskTargets = new RemoteAnimationTarget[runningTaskIds.length]; 152 for (int i = 0; i < runningTaskIds.length; i++) { 153 RemoteAnimationTarget task = mTargets.findTask(runningTaskIds[i]); 154 mLastAppearedTaskTargets[i] = task; 155 } 156 mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets); 157 } 158 159 @Override 160 public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) { 161 cleanUpRecentsAnimation(newCallbacks); 162 } 163 164 @Override 165 public void onRecentsAnimationFinished(RecentsAnimationController controller) { 166 cleanUpRecentsAnimation(newCallbacks); 167 } 168 169 @Override 170 public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) { 171 RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0]; 172 BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); 173 174 for (RemoteAnimationTarget compat : appearedTaskTargets) { 175 if (compat.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME 176 && activityInterface.getCreatedActivity() instanceof RecentsActivity 177 && DisplayController.getNavigationMode(mCtx) != NO_BUTTON) { 178 // The only time we get onTasksAppeared() in button navigation with a 179 // 3p launcher is if the user goes to overview first, and in this case we 180 // can immediately finish the transition 181 RecentsView recentsView = 182 activityInterface.getCreatedActivity().getOverviewPanel(); 183 if (recentsView != null) { 184 Log.d(TestProtocol.INCORRECT_HOME_STATE, 185 "finish recents animation on " 186 + compat.taskInfo.description); 187 recentsView.finishRecentsAnimation(true, null); 188 } 189 return; 190 } 191 } 192 193 RemoteAnimationTarget[] nonAppTargets = SystemUiProxy.INSTANCE.get(mCtx) 194 .onStartingSplitLegacy(appearedTaskTargets); 195 if (nonAppTargets == null) { 196 nonAppTargets = new RemoteAnimationTarget[0]; 197 } 198 if ((activityInterface.isInLiveTileMode() 199 || mLastGestureState.getEndTarget() == RECENTS) 200 && activityInterface.getCreatedActivity() != null) { 201 RecentsView recentsView = 202 activityInterface.getCreatedActivity().getOverviewPanel(); 203 if (recentsView != null) { 204 ActiveGestureLog.INSTANCE.addLog("Launching side task id=" 205 + appearedTaskTarget.taskId); 206 recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, 207 appearedTaskTargets, 208 new RemoteAnimationTarget[0] /* wallpaper */, 209 nonAppTargets /* nonApps */); 210 return; 211 } else { 212 ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)"); 213 } 214 } else if (nonAppTargets.length > 0) { 215 TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets /* nonApps */, 216 true /*shown*/, null /* animatorHandler */); 217 } 218 if (mController != null) { 219 if (mLastAppearedTaskTargets != null) { 220 for (RemoteAnimationTarget lastTarget : mLastAppearedTaskTargets) { 221 for (RemoteAnimationTarget appearedTarget : appearedTaskTargets) { 222 if (lastTarget != null && 223 appearedTarget.taskId != lastTarget.taskId) { 224 mController.removeTaskTarget(lastTarget.taskId); 225 } 226 } 227 } 228 } 229 mLastAppearedTaskTargets = appearedTaskTargets; 230 mLastGestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets); 231 } 232 } 233 234 @Override 235 public boolean onSwitchToScreenshot(Runnable onFinished) { 236 if (!activityInterface.isInLiveTileMode() 237 || activityInterface.getCreatedActivity() == null) { 238 // No need to switch since tile is already a screenshot. 239 onFinished.run(); 240 } else { 241 final RecentsView recentsView = 242 activityInterface.getCreatedActivity().getOverviewPanel(); 243 if (recentsView != null) { 244 recentsView.switchToScreenshot(onFinished); 245 } else { 246 onFinished.run(); 247 } 248 } 249 return true; 250 } 251 }); 252 final long eventTime = gestureState.getSwipeUpStartTimeMs(); 253 mCallbacks.addListener(gestureState); 254 mCallbacks.addListener(listener); 255 256 if (ENABLE_SHELL_TRANSITIONS) { 257 final ActivityOptions options = ActivityOptions.makeBasic(); 258 // Allowing to pause Home if Home is top activity and Recents is not Home. So when user 259 // start home when recents animation is playing, the home activity can be resumed again 260 // to let the transition controller collect Home activity. 261 CachedTaskInfo cti = gestureState.getRunningTask(); 262 boolean homeIsOnTop = cti != null && cti.isHomeTask(); 263 if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) { 264 if (cti != null && cti.isFreeformTask()) { 265 // No transient launch when desktop task is on top 266 homeIsOnTop = true; 267 } 268 } 269 if (activityInterface.allowAllAppsFromOverview()) { 270 homeIsOnTop = true; 271 } 272 if (!homeIsOnTop) { 273 options.setTransientLaunch(); 274 } 275 options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime); 276 SystemUiProxy.INSTANCE.getNoCreate().startRecentsActivity(intent, options, mCallbacks); 277 } else { 278 UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance() 279 .startRecentsActivity(intent, eventTime, mCallbacks, null, null)); 280 } 281 gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED); 282 return mCallbacks; 283 } 284 285 /** 286 * Continues the existing running recents animation for a new gesture. 287 */ continueRecentsAnimation(GestureState gestureState)288 public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) { 289 ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation"); 290 mCallbacks.removeListener(mLastGestureState); 291 mLastGestureState = gestureState; 292 mCallbacks.addListener(gestureState); 293 gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED 294 | STATE_RECENTS_ANIMATION_STARTED); 295 gestureState.updateLastAppearedTaskTargets(mLastAppearedTaskTargets); 296 return mCallbacks; 297 } 298 endLiveTile()299 public void endLiveTile() { 300 if (mLastGestureState == null) { 301 return; 302 } 303 BaseActivityInterface activityInterface = mLastGestureState.getActivityInterface(); 304 if (activityInterface.isInLiveTileMode() 305 && activityInterface.getCreatedActivity() != null) { 306 RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); 307 if (recentsView != null) { 308 recentsView.switchToScreenshot(null, 309 () -> recentsView.finishRecentsAnimation(true /* toRecents */, 310 false /* shouldPip */, null)); 311 } 312 } 313 } 314 setLiveTileCleanUpHandler(Runnable cleanUpHandler)315 public void setLiveTileCleanUpHandler(Runnable cleanUpHandler) { 316 mLiveTileCleanUpHandler = cleanUpHandler; 317 } 318 enableLiveTileRestartListener()319 public void enableLiveTileRestartListener() { 320 TaskStackChangeListeners.getInstance().registerTaskStackListener(mLiveTileRestartListener); 321 } 322 323 /** 324 * Finishes the running recents animation. 325 */ finishRunningRecentsAnimation(boolean toHome)326 public void finishRunningRecentsAnimation(boolean toHome) { 327 finishRunningRecentsAnimation(toHome, false /* forceFinish */); 328 } 329 330 /** 331 * Finishes the running recents animation. 332 * @param forceFinish will synchronously finish the controller 333 */ finishRunningRecentsAnimation(boolean toHome, boolean forceFinish)334 public void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish) { 335 if (mController != null) { 336 ActiveGestureLog.INSTANCE.addLog( 337 /* event= */ "finishRunningRecentsAnimation", toHome); 338 if (forceFinish) { 339 mController.finishController(toHome, null, false /* sendUserLeaveHint */, 340 true /* forceFinish */); 341 } else { 342 Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome 343 ? mController::finishAnimationToHome 344 : mController::finishAnimationToApp); 345 } 346 } 347 } 348 349 /** 350 * Used to notify a listener of the current recents animation state (used if the listener was 351 * not yet added to the callbacks at the point that the listener callbacks would have been 352 * made). 353 */ notifyRecentsAnimationState( RecentsAnimationCallbacks.RecentsAnimationListener listener)354 public void notifyRecentsAnimationState( 355 RecentsAnimationCallbacks.RecentsAnimationListener listener) { 356 if (isRecentsAnimationRunning()) { 357 listener.onRecentsAnimationStart(mController, mTargets); 358 } 359 // TODO: Do we actually need to report canceled/finished? 360 } 361 362 /** 363 * @return whether there is a recents animation running. 364 */ isRecentsAnimationRunning()365 public boolean isRecentsAnimationRunning() { 366 return mController != null; 367 } 368 369 /** 370 * Cleans up the recents animation entirely. 371 */ cleanUpRecentsAnimation(RecentsAnimationCallbacks targetCallbacks)372 private void cleanUpRecentsAnimation(RecentsAnimationCallbacks targetCallbacks) { 373 if (mCallbacks != targetCallbacks) { 374 ActiveGestureLog.INSTANCE.addLog( 375 /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks"); 376 return; 377 } 378 ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation"); 379 if (mLiveTileCleanUpHandler != null) { 380 mLiveTileCleanUpHandler.run(); 381 mLiveTileCleanUpHandler = null; 382 } 383 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mLiveTileRestartListener); 384 385 // Release all the target leashes 386 if (mTargets != null) { 387 mTargets.release(); 388 } 389 390 // Clean up all listeners to ensure we don't get subsequent callbacks 391 if (mCallbacks != null) { 392 mCallbacks.removeAllListeners(); 393 } 394 395 mController = null; 396 mCallbacks = null; 397 mTargets = null; 398 mLastGestureState = null; 399 mLastAppearedTaskTargets = null; 400 } 401 402 @Nullable getCurrentCallbacks()403 public RecentsAnimationCallbacks getCurrentCallbacks() { 404 return mCallbacks; 405 } 406 dump()407 public void dump() { 408 // TODO 409 } 410 } 411