1 /* 2 * Copyright (C) 2022 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 17 package com.android.wm.shell.transition; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 20 import static android.view.WindowManager.TRANSIT_CHANGE; 21 import static android.view.WindowManager.TRANSIT_TO_BACK; 22 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; 23 24 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; 25 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; 26 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; 27 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.os.IBinder; 31 import android.view.SurfaceControl; 32 import android.view.WindowManager; 33 import android.window.TransitionInfo; 34 import android.window.TransitionRequestInfo; 35 import android.window.WindowContainerTransaction; 36 37 import com.android.internal.protolog.common.ProtoLog; 38 import com.android.wm.shell.pip.PipTransitionController; 39 import com.android.wm.shell.pip.phone.PipTouchHandler; 40 import com.android.wm.shell.protolog.ShellProtoLogGroup; 41 import com.android.wm.shell.splitscreen.SplitScreenController; 42 import com.android.wm.shell.splitscreen.StageCoordinator; 43 import com.android.wm.shell.sysui.ShellInit; 44 45 import java.util.ArrayList; 46 import java.util.Optional; 47 48 /** 49 * A handler for dealing with transitions involving multiple other handlers. For example: an 50 * activity in split-screen going into PiP. 51 */ 52 public class DefaultMixedHandler implements Transitions.TransitionHandler { 53 54 private final Transitions mPlayer; 55 private PipTransitionController mPipHandler; 56 private StageCoordinator mSplitHandler; 57 58 private static class MixedTransition { 59 static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; 60 61 /** Both the display and split-state (enter/exit) is changing */ 62 static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2; 63 64 /** The default animation for this mixed transition. */ 65 static final int ANIM_TYPE_DEFAULT = 0; 66 67 /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */ 68 static final int ANIM_TYPE_GOING_HOME = 1; 69 70 final int mType; 71 int mAnimType = 0; 72 final IBinder mTransition; 73 74 Transitions.TransitionFinishCallback mFinishCallback = null; 75 Transitions.TransitionHandler mLeftoversHandler = null; 76 WindowContainerTransaction mFinishWCT = null; 77 78 /** 79 * Mixed transitions are made up of multiple "parts". This keeps track of how many 80 * parts are currently animating. 81 */ 82 int mInFlightSubAnimations = 0; 83 MixedTransition(int type, IBinder transition)84 MixedTransition(int type, IBinder transition) { 85 mType = type; 86 mTransition = transition; 87 } 88 } 89 90 private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>(); 91 DefaultMixedHandler(@onNull ShellInit shellInit, @NonNull Transitions player, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipTouchHandler> pipTouchHandlerOptional)92 public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, 93 Optional<SplitScreenController> splitScreenControllerOptional, 94 Optional<PipTouchHandler> pipTouchHandlerOptional) { 95 mPlayer = player; 96 if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent() 97 && splitScreenControllerOptional.isPresent()) { 98 // Add after dependencies because it is higher priority 99 shellInit.addInitCallback(() -> { 100 mPipHandler = pipTouchHandlerOptional.get().getTransitionHandler(); 101 mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler(); 102 mPlayer.addHandler(this); 103 if (mSplitHandler != null) { 104 mSplitHandler.setMixedHandler(this); 105 } 106 }, this); 107 } 108 } 109 110 @Nullable 111 @Override handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)112 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 113 @NonNull TransitionRequestInfo request) { 114 if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) { 115 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while " 116 + "Split-Screen is active, so treat it as Mixed."); 117 if (request.getRemoteTransition() != null) { 118 throw new IllegalStateException("Unexpected remote transition in" 119 + "pip-enter-from-split request"); 120 } 121 mActiveTransitions.add(new MixedTransition(MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, 122 transition)); 123 124 WindowContainerTransaction out = new WindowContainerTransaction(); 125 mPipHandler.augmentRequest(transition, request, out); 126 mSplitHandler.addEnterOrExitIfNeeded(request, out); 127 return out; 128 } 129 return null; 130 } 131 subCopy(@onNull TransitionInfo info, @WindowManager.TransitionType int newType, boolean withChanges)132 private TransitionInfo subCopy(@NonNull TransitionInfo info, 133 @WindowManager.TransitionType int newType, boolean withChanges) { 134 final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0); 135 if (withChanges) { 136 for (int i = 0; i < info.getChanges().size(); ++i) { 137 out.getChanges().add(info.getChanges().get(i)); 138 } 139 } 140 out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y); 141 out.setAnimationOptions(info.getAnimationOptions()); 142 return out; 143 } 144 isHomeOpening(@onNull TransitionInfo.Change change)145 private boolean isHomeOpening(@NonNull TransitionInfo.Change change) { 146 return change.getTaskInfo() != null 147 && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME; 148 } 149 isWallpaper(@onNull TransitionInfo.Change change)150 private boolean isWallpaper(@NonNull TransitionInfo.Change change) { 151 return (change.getFlags() & FLAG_IS_WALLPAPER) != 0; 152 } 153 154 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)155 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 156 @NonNull SurfaceControl.Transaction startTransaction, 157 @NonNull SurfaceControl.Transaction finishTransaction, 158 @NonNull Transitions.TransitionFinishCallback finishCallback) { 159 MixedTransition mixed = null; 160 for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { 161 if (mActiveTransitions.get(i).mTransition != transition) continue; 162 mixed = mActiveTransitions.get(i); 163 break; 164 } 165 if (mixed == null) return false; 166 167 if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { 168 return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction, 169 finishCallback); 170 } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { 171 return false; 172 } else { 173 mActiveTransitions.remove(mixed); 174 throw new IllegalStateException("Starting mixed animation without a known mixed type? " 175 + mixed.mType); 176 } 177 } 178 animateEnterPipFromSplit(@onNull final MixedTransition mixed, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)179 private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed, 180 @NonNull TransitionInfo info, 181 @NonNull SurfaceControl.Transaction startTransaction, 182 @NonNull SurfaceControl.Transaction finishTransaction, 183 @NonNull Transitions.TransitionFinishCallback finishCallback) { 184 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " 185 + "entering PIP while Split-Screen is active."); 186 TransitionInfo.Change pipChange = null; 187 TransitionInfo.Change wallpaper = null; 188 final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); 189 boolean homeIsOpening = false; 190 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 191 TransitionInfo.Change change = info.getChanges().get(i); 192 if (mPipHandler.isEnteringPip(change, info.getType())) { 193 if (pipChange != null) { 194 throw new IllegalStateException("More than 1 pip-entering changes in one" 195 + " transition? " + info); 196 } 197 pipChange = change; 198 // going backwards, so remove-by-index is fine. 199 everythingElse.getChanges().remove(i); 200 } else if (isHomeOpening(change)) { 201 homeIsOpening = true; 202 } else if (isWallpaper(change)) { 203 wallpaper = change; 204 } 205 } 206 if (pipChange == null) { 207 // um, something probably went wrong. 208 return false; 209 } 210 final boolean isGoingHome = homeIsOpening; 211 mixed.mFinishCallback = finishCallback; 212 Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { 213 --mixed.mInFlightSubAnimations; 214 if (mixed.mInFlightSubAnimations > 0) return; 215 mActiveTransitions.remove(mixed); 216 if (isGoingHome) { 217 mSplitHandler.onTransitionAnimationComplete(); 218 } 219 mixed.mFinishCallback.onTransitionFinished(wct, wctCB); 220 }; 221 if (isGoingHome) { 222 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed " 223 + "since entering-PiP caused us to leave split and return home."); 224 // We need to split the transition into 2 parts: the pip part (animated by pip) 225 // and the dismiss-part (animated by launcher). 226 mixed.mInFlightSubAnimations = 2; 227 // immediately make the wallpaper visible (so that we don't see it pop-in during 228 // the time it takes to start recents animation (which is remote). 229 if (wallpaper != null) { 230 startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f); 231 } 232 // make a new startTransaction because pip's startEnterAnimation "consumes" it so 233 // we need a separate one to send over to launcher. 234 SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); 235 // Let split update internal state for dismiss. 236 mSplitHandler.prepareDismissAnimation(STAGE_TYPE_UNDEFINED, 237 EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, 238 finishTransaction); 239 240 // We are trying to accommodate launcher's close animation which can't handle the 241 // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove 242 // from transition info. 243 for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) { 244 if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) { 245 everythingElse.getChanges().remove(i); 246 break; 247 } 248 } 249 250 mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, 251 finishCB); 252 // Dispatch the rest of the transition normally. This will most-likely be taken by 253 // recents or default handler. 254 mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse, 255 otherStartT, finishTransaction, finishCB, this); 256 } else { 257 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " 258 + "forward animation to Pip-Handler."); 259 // This happens if the pip-ing activity is in a multi-activity task (and thus a 260 // new pip task is spawned). In this case, we don't actually exit split so we can 261 // just let pip transition handle the animation verbatim. 262 mixed.mInFlightSubAnimations = 1; 263 mPipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction, 264 finishCB); 265 } 266 return true; 267 } 268 unlinkMissingParents(TransitionInfo from)269 private void unlinkMissingParents(TransitionInfo from) { 270 for (int i = 0; i < from.getChanges().size(); ++i) { 271 final TransitionInfo.Change chg = from.getChanges().get(i); 272 if (chg.getParent() == null) continue; 273 if (from.getChange(chg.getParent()) == null) { 274 from.getChanges().get(i).setParent(null); 275 } 276 } 277 } 278 isWithinTask(TransitionInfo info, TransitionInfo.Change chg)279 private boolean isWithinTask(TransitionInfo info, TransitionInfo.Change chg) { 280 TransitionInfo.Change curr = chg; 281 while (curr != null) { 282 if (curr.getTaskInfo() != null) return true; 283 if (curr.getParent() == null) break; 284 curr = info.getChange(curr.getParent()); 285 } 286 return false; 287 } 288 289 /** 290 * This is intended to be called by SplitCoordinator as a helper to mix an already-pending 291 * split transition with a display-change. The use-case for this is when a display 292 * change/rotation gets collected into a split-screen enter/exit transition which has already 293 * been claimed by StageCoordinator.handleRequest . This happens during launcher tests. 294 */ animatePendingSplitWithDisplayChange(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)295 public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition, 296 @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, 297 @NonNull SurfaceControl.Transaction finishT, 298 @NonNull Transitions.TransitionFinishCallback finishCallback) { 299 final TransitionInfo everythingElse = subCopy(info, info.getType(), true /* withChanges */); 300 final TransitionInfo displayPart = subCopy(info, TRANSIT_CHANGE, false /* withChanges */); 301 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 302 TransitionInfo.Change change = info.getChanges().get(i); 303 if (isWithinTask(info, change)) continue; 304 displayPart.addChange(change); 305 everythingElse.getChanges().remove(i); 306 } 307 if (displayPart.getChanges().isEmpty()) return false; 308 unlinkMissingParents(everythingElse); 309 final MixedTransition mixed = new MixedTransition( 310 MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition); 311 mixed.mFinishCallback = finishCallback; 312 mActiveTransitions.add(mixed); 313 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change " 314 + "and split change."); 315 // We need to split the transition into 2 parts: the split part and the display part. 316 mixed.mInFlightSubAnimations = 2; 317 318 Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> { 319 --mixed.mInFlightSubAnimations; 320 if (wctCB != null) { 321 throw new IllegalArgumentException("Can't mix transitions that require finish" 322 + " sync callback"); 323 } 324 if (wct != null) { 325 if (mixed.mFinishWCT == null) { 326 mixed.mFinishWCT = wct; 327 } else { 328 mixed.mFinishWCT.merge(wct, true /* transfer */); 329 } 330 } 331 if (mixed.mInFlightSubAnimations > 0) return; 332 mActiveTransitions.remove(mixed); 333 mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */); 334 }; 335 336 // Dispatch the display change. This will most-likely be taken by the default handler. 337 // Do this first since the first handler used will apply the startT; the display change 338 // needs to take a screenshot before that happens so we need it to be the first handler. 339 mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, displayPart, 340 startT, finishT, finishCB, mSplitHandler); 341 342 // Note: at this point, startT has probably already been applied, so we are basically 343 // giving splitHandler an empty startT. This is currently OK because display-change will 344 // grab a screenshot and paste it on top anyways. 345 mSplitHandler.startPendingAnimation( 346 transition, everythingElse, startT, finishT, finishCB); 347 return true; 348 } 349 350 @Override mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)351 public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 352 @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, 353 @NonNull Transitions.TransitionFinishCallback finishCallback) { 354 for (int i = 0; i < mActiveTransitions.size(); ++i) { 355 if (mActiveTransitions.get(i) != mergeTarget) continue; 356 MixedTransition mixed = mActiveTransitions.get(i); 357 if (mixed.mInFlightSubAnimations <= 0) { 358 // Already done, so no need to end it. 359 return; 360 } 361 if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { 362 if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) { 363 boolean ended = mSplitHandler.end(); 364 // If split couldn't end (because it is remote), then don't end everything else 365 // since we have to play out the animation anyways. 366 if (!ended) return; 367 mPipHandler.end(); 368 if (mixed.mLeftoversHandler != null) { 369 mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, 370 finishCallback); 371 } 372 } else { 373 mPipHandler.end(); 374 } 375 } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) { 376 // queue 377 } else { 378 throw new IllegalStateException("Playing a mixed transition with unknown type? " 379 + mixed.mType); 380 } 381 } 382 } 383 384 @Override onTransitionConsumed(@onNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishT)385 public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, 386 @Nullable SurfaceControl.Transaction finishT) { 387 MixedTransition mixed = null; 388 for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { 389 if (mActiveTransitions.get(i).mTransition != transition) continue; 390 mixed = mActiveTransitions.remove(i); 391 break; 392 } 393 if (mixed == null) return; 394 if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) { 395 mPipHandler.onTransitionConsumed(transition, aborted, finishT); 396 } 397 } 398 } 399