1 /* 2 * Copyright (C) 2024 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.systemui.animation; 18 19 import static android.view.WindowManager.TRANSIT_CHANGE; 20 21 import android.animation.Animator; 22 import android.animation.Animator.AnimatorListener; 23 import android.animation.ValueAnimator; 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.graphics.Rect; 27 import android.hardware.display.DisplayManager; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.util.DisplayMetrics; 32 import android.util.Log; 33 import android.view.SurfaceControl; 34 import android.window.IRemoteTransition; 35 import android.window.IRemoteTransitionFinishedCallback; 36 import android.window.TransitionInfo; 37 import android.window.TransitionInfo.Change; 38 import android.window.WindowAnimationState; 39 40 import com.android.internal.policy.ScreenDecorationsUtils; 41 import com.android.wm.shell.shared.TransitionUtil; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.concurrent.atomic.AtomicBoolean; 47 48 /** 49 * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin 50 * and automatically attaches it to the transition leash before the transition starts. 51 * 52 * @hide 53 */ 54 public class OriginRemoteTransition extends IRemoteTransition.Stub { 55 private static final String TAG = "OriginRemoteTransition"; 56 private static final long FINISH_ANIMATION_TIMEOUT_MS = 100; 57 58 private final Context mContext; 59 private final boolean mIsEntry; 60 private final UIComponent mOrigin; 61 private final TransitionPlayer mPlayer; 62 private final long mDuration; 63 private final Handler mHandler; 64 65 @Nullable private SurfaceControl.Transaction mStartTransaction; 66 @Nullable private IRemoteTransitionFinishedCallback mFinishCallback; 67 @Nullable private UIComponent.Transaction mOriginTransaction; 68 @Nullable private ValueAnimator mAnimator; 69 @Nullable private SurfaceControl mOriginLeash; 70 private boolean mCancelled; 71 OriginRemoteTransition( Context context, boolean isEntry, UIComponent origin, TransitionPlayer player, long duration, Handler handler)72 OriginRemoteTransition( 73 Context context, 74 boolean isEntry, 75 UIComponent origin, 76 TransitionPlayer player, 77 long duration, 78 Handler handler) { 79 mContext = context; 80 mIsEntry = isEntry; 81 mOrigin = origin; 82 mPlayer = player; 83 mDuration = duration; 84 mHandler = handler; 85 } 86 87 @Override startAnimation( IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)88 public void startAnimation( 89 IBinder token, 90 TransitionInfo info, 91 SurfaceControl.Transaction t, 92 IRemoteTransitionFinishedCallback finishCallback) { 93 logD("startAnimation - " + info); 94 mHandler.post( 95 () -> { 96 mStartTransaction = t; 97 mFinishCallback = finishCallback; 98 startAnimationInternal(info, /* states= */ null); 99 }); 100 } 101 102 @Override mergeAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback)103 public void mergeAnimation( 104 IBinder transition, 105 TransitionInfo info, 106 SurfaceControl.Transaction t, 107 IBinder mergeTarget, 108 IRemoteTransitionFinishedCallback finishCallback) { 109 logD("mergeAnimation - " + info); 110 cancel(); 111 } 112 113 @Override takeOverAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)114 public void takeOverAnimation( 115 IBinder transition, 116 TransitionInfo info, 117 SurfaceControl.Transaction t, 118 IRemoteTransitionFinishedCallback finishCallback, 119 WindowAnimationState[] states) { 120 logD("takeOverAnimation - info=" + info + ", states=" + Arrays.toString(states)); 121 mHandler.post( 122 () -> { 123 mStartTransaction = t; 124 mFinishCallback = finishCallback; 125 startAnimationInternal(info, states); 126 }); 127 } 128 129 @Override onTransitionConsumed(IBinder transition, boolean aborted)130 public void onTransitionConsumed(IBinder transition, boolean aborted) { 131 logD("onTransitionConsumed - aborted: " + aborted); 132 cancel(); 133 } 134 startAnimationInternal( TransitionInfo info, @Nullable WindowAnimationState[] states)135 private void startAnimationInternal( 136 TransitionInfo info, @Nullable WindowAnimationState[] states) { 137 if (!prepareUIs(info)) { 138 logE("Unable to prepare UI!"); 139 finishAnimation(/* finished= */ false); 140 return; 141 } 142 // Notify player that we are starting. 143 mPlayer.onStart(info, states, mStartTransaction, mOrigin, mOriginTransaction); 144 145 // Apply the initial transactions in case the player forgot to apply them. 146 mOriginTransaction.commit(); 147 mStartTransaction.apply(); 148 149 // Start the animator. 150 mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 151 mAnimator.setDuration(mDuration); 152 mAnimator.addListener( 153 new AnimatorListener() { 154 @Override 155 public void onAnimationStart(Animator a) {} 156 157 @Override 158 public void onAnimationEnd(Animator a) { 159 finishAnimation(/* finished= */ !mCancelled); 160 } 161 162 @Override 163 public void onAnimationCancel(Animator a) { 164 mCancelled = true; 165 } 166 167 @Override 168 public void onAnimationRepeat(Animator a) {} 169 }); 170 mAnimator.addUpdateListener( 171 a -> { 172 mPlayer.onProgress((float) a.getAnimatedValue()); 173 }); 174 mAnimator.start(); 175 } 176 prepareUIs(TransitionInfo info)177 private boolean prepareUIs(TransitionInfo info) { 178 if (info.getRootCount() == 0) { 179 logE("prepareUIs: no root leash!"); 180 return false; 181 } 182 if (info.getRootCount() > 1) { 183 logE("prepareUIs: multi-display transition is not supported yet!"); 184 return false; 185 } 186 if (info.getChanges().isEmpty()) { 187 logE("prepareUIs: no changes!"); 188 return false; 189 } 190 191 SurfaceControl rootLeash = info.getRoot(0).getLeash(); 192 int displayId = info.getChanges().get(0).getEndDisplayId(); 193 Rect displayBounds = getDisplayBounds(displayId); 194 float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); 195 logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds); 196 197 // Create the origin leash and add to the transition root leash. 198 mOriginLeash = 199 new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build(); 200 201 // Create temporary transaction to build 202 final SurfaceControl.Transaction tmpTransaction = new SurfaceControl.Transaction(); 203 tmpTransaction 204 .reparent(mOriginLeash, rootLeash) 205 .show(mOriginLeash) 206 .setCornerRadius(mOriginLeash, windowRadius) 207 .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height()); 208 209 // Process surfaces 210 List<SurfaceControl> openingSurfaces = new ArrayList<>(); 211 List<SurfaceControl> closingSurfaces = new ArrayList<>(); 212 for (Change change : info.getChanges()) { 213 int mode = change.getMode(); 214 SurfaceControl leash = change.getLeash(); 215 // Reparent leash to the transition root. 216 tmpTransaction.reparent(leash, rootLeash); 217 if (TransitionUtil.isOpeningMode(mode)) { 218 openingSurfaces.add(change.getLeash()); 219 // For opening surfaces, ending bounds are base bound. Apply corner radius if 220 // it's full screen. 221 Rect bounds = change.getEndAbsBounds(); 222 if (displayBounds.equals(bounds)) { 223 tmpTransaction 224 .setCornerRadius(leash, windowRadius) 225 .setWindowCrop(leash, bounds.width(), bounds.height()); 226 } 227 } else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) { 228 // TRANSIT_CHANGE refers to the closing window in predictive back animation. 229 closingSurfaces.add(change.getLeash()); 230 // For closing surfaces, starting bounds are base bounds. Apply corner radius if 231 // it's full screen. 232 Rect bounds = change.getStartAbsBounds(); 233 if (displayBounds.equals(bounds)) { 234 tmpTransaction 235 .setCornerRadius(leash, windowRadius) 236 .setWindowCrop(leash, bounds.width(), bounds.height()); 237 } 238 } 239 } 240 241 if (openingSurfaces.isEmpty() && closingSurfaces.isEmpty()) { 242 logD("prepareUIs: no opening/closing surfaces available, nothing to prepare."); 243 return false; 244 } 245 246 // Set relative order: 247 // ---- App1 ---- 248 // ---- origin ---- 249 // ---- App2 ---- 250 251 if (mIsEntry) { 252 if (!closingSurfaces.isEmpty()) { 253 tmpTransaction.setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1); 254 } else { 255 logW("Missing closing surface is entry transition"); 256 } 257 if (!openingSurfaces.isEmpty()) { 258 tmpTransaction.setRelativeLayer( 259 openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); 260 } else { 261 logW("Missing opening surface is entry transition"); 262 } 263 264 } else { 265 if (!openingSurfaces.isEmpty()) { 266 tmpTransaction.setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1); 267 } else { 268 logW("Missing opening surface is exit transition"); 269 } 270 if (!closingSurfaces.isEmpty()) { 271 tmpTransaction.setRelativeLayer( 272 closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); 273 } else { 274 logW("Missing closing surface is exit transition"); 275 } 276 } 277 mStartTransaction.merge(tmpTransaction); 278 279 // Attach origin UIComponent to origin leash. 280 mOriginTransaction = mOrigin.newTransaction(); 281 mOriginTransaction.attachToTransitionLeash( 282 mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height()); 283 return true; 284 } 285 getDisplayBounds(int displayId)286 private Rect getDisplayBounds(int displayId) { 287 DisplayManager dm = mContext.getSystemService(DisplayManager.class); 288 DisplayMetrics metrics = new DisplayMetrics(); 289 dm.getDisplay(displayId).getMetrics(metrics); 290 return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels); 291 } 292 finishAnimation(boolean finished)293 private void finishAnimation(boolean finished) { 294 logD("finishAnimation: finished=" + finished); 295 OneShotRunnable finishInternalRunnable = new OneShotRunnable(this::finishInternal); 296 Runnable timeoutRunnable = 297 () -> { 298 Log.w(TAG, "Timeout waiting for surface transaction!"); 299 finishInternalRunnable.run(); 300 }; 301 Runnable committedRunnable = 302 () -> { 303 // Remove the timeout runnable. 304 mHandler.removeCallbacks(timeoutRunnable); 305 finishInternalRunnable.run(); 306 }; 307 if (mAnimator == null) { 308 // The transition didn't start. Ensure we apply the start transaction and report 309 // finish afterwards. 310 mStartTransaction 311 .addTransactionCommittedListener(mHandler::post, committedRunnable::run) 312 .apply(); 313 // Call finishInternal() anyway after the timeout. 314 mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS); 315 return; 316 } 317 mAnimator = null; 318 // Notify client that we have ended. 319 mPlayer.onEnd(finished); 320 // Detach the origin from the transition leash and report finish after it's done. 321 mOriginTransaction 322 .detachFromTransitionLeash(mOrigin, mHandler::post, committedRunnable) 323 .commit(); 324 // Call finishInternal() anyway after the timeout. 325 mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS); 326 } 327 finishInternal()328 private void finishInternal() { 329 logD("finishInternal"); 330 if (mOriginLeash != null) { 331 // Release origin leash. 332 mOriginLeash.release(); 333 mOriginLeash = null; 334 } 335 try { 336 mFinishCallback.onTransitionFinished(null, null); 337 } catch (RemoteException e) { 338 logE("Unable to report transition finish!", e); 339 } 340 mStartTransaction = null; 341 mOriginTransaction = null; 342 mFinishCallback = null; 343 } 344 cancel()345 public void cancel() { 346 logD("cancel()"); 347 mHandler.post( 348 () -> { 349 if (mAnimator != null) { 350 mAnimator.cancel(); 351 } 352 }); 353 } 354 logD(String msg)355 private static void logD(String msg) { 356 if (OriginTransitionSession.DEBUG) { 357 Log.d(TAG, msg); 358 } 359 } 360 logW(String msg)361 private static void logW(String msg) { 362 Log.w(TAG, msg); 363 } 364 logE(String msg)365 private static void logE(String msg) { 366 Log.e(TAG, msg); 367 } 368 logE(String msg, Throwable e)369 private static void logE(String msg, Throwable e) { 370 Log.e(TAG, msg, e); 371 } 372 wrapSurfaces(TransitionInfo info, boolean isOpening)373 private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) { 374 List<SurfaceControl> surfaces = new ArrayList<>(); 375 Rect maxBounds = new Rect(); 376 for (Change change : info.getChanges()) { 377 int mode = change.getMode(); 378 if (TransitionUtil.isOpeningMode(mode) == isOpening) { 379 surfaces.add(change.getLeash()); 380 Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds(); 381 maxBounds.union(bounds); 382 } 383 } 384 return new SurfaceUIComponent( 385 surfaces, 386 /* alpha= */ 1.0f, 387 /* visible= */ true, 388 /* bounds= */ maxBounds, 389 /* baseBounds= */ maxBounds); 390 } 391 applyWindowAnimationStates( TransitionInfo info, @Nullable WindowAnimationState[] states, UIComponent closingApp, UIComponent openingApp)392 private static void applyWindowAnimationStates( 393 TransitionInfo info, 394 @Nullable WindowAnimationState[] states, 395 UIComponent closingApp, 396 UIComponent openingApp) { 397 if (states == null) { 398 // Nothing to apply. 399 return; 400 } 401 // Calculate bounds. 402 Rect maxClosingBounds = new Rect(); 403 Rect maxOpeningBounds = new Rect(); 404 for (int i = 0; i < info.getChanges().size(); i++) { 405 Rect bound = getBounds(states[i]); 406 if (bound == null) { 407 continue; 408 } 409 int mode = info.getChanges().get(i).getMode(); 410 if (TransitionUtil.isOpeningMode(mode)) { 411 maxOpeningBounds.union(bound); 412 } else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) { 413 // TRANSIT_CHANGE refers to the closing window in predictive back animation. 414 maxClosingBounds.union(bound); 415 } 416 } 417 418 // Intentionally use a new transaction instead of reusing the existing transaction since we 419 // want to apply window animation states first without committing any other pending changes 420 // in the existing transaction. The existing transaction is expected to be committed by the 421 // onStart() client callback together with client's custom transformation. 422 UIComponent.Transaction transaction = closingApp.newTransaction(); 423 if (!maxClosingBounds.isEmpty()) { 424 logD("Applying closing window bounds: " + maxClosingBounds); 425 transaction.setBounds(closingApp, maxClosingBounds); 426 } 427 if (!maxOpeningBounds.isEmpty()) { 428 logD("Applying opening window bounds: " + maxOpeningBounds); 429 transaction.setBounds(openingApp, maxOpeningBounds); 430 } 431 transaction.commit(); 432 } 433 434 @Nullable getBounds(@ullable WindowAnimationState state)435 private static Rect getBounds(@Nullable WindowAnimationState state) { 436 if (state == null || state.bounds == null) { 437 return null; 438 } 439 Rect out = new Rect(); 440 state.bounds.roundOut(out); 441 return out; 442 } 443 444 /** A {@link Runnable} that will only run once. */ 445 private static class OneShotRunnable implements Runnable { 446 private final AtomicBoolean mDone = new AtomicBoolean(); 447 private final Runnable mRunnable; 448 OneShotRunnable(Runnable runnable)449 OneShotRunnable(Runnable runnable) { 450 this.mRunnable = runnable; 451 } 452 453 @Override run()454 public void run() { 455 if (!mDone.getAndSet(true)) { 456 mRunnable.run(); 457 } 458 } 459 } 460 461 /** 462 * An interface that represents an origin transitions. 463 * 464 * @hide 465 */ 466 public interface TransitionPlayer { 467 468 /** 469 * Called when an origin transition starts. This method exposes the raw {@link 470 * TransitionInfo} so that clients can extract more information from it. 471 * 472 * <p>Note: if this transition is taking over a predictive back animation, the {@link 473 * WindowAnimationState} will be passed to this method. The concrete implementation is 474 * expected to apply the {@link WindowAnimationState} before continuing the transition. 475 */ onStart( TransitionInfo transitionInfo, @Nullable WindowAnimationState[] states, SurfaceControl.Transaction sfTransaction, UIComponent origin, UIComponent.Transaction uiTransaction)476 default void onStart( 477 TransitionInfo transitionInfo, 478 @Nullable WindowAnimationState[] states, 479 SurfaceControl.Transaction sfTransaction, 480 UIComponent origin, 481 UIComponent.Transaction uiTransaction) { 482 // Wrap transactions. 483 Transactions transactions = 484 new Transactions() 485 .registerTransactionForClass(origin.getClass(), uiTransaction) 486 .registerTransactionForClass( 487 SurfaceUIComponent.class, 488 new SurfaceUIComponent.Transaction(sfTransaction)); 489 // Wrap surfaces. 490 UIComponent closingApp = wrapSurfaces(transitionInfo, /* isOpening= */ false); 491 UIComponent openingApp = wrapSurfaces(transitionInfo, /* isOpening= */ true); 492 493 // Restore the pending animation states coming from predictive back transition. 494 applyWindowAnimationStates(transitionInfo, states, closingApp, openingApp); 495 496 // Start. 497 onStart(transactions, origin, closingApp, openingApp); 498 } 499 500 /** 501 * Called when an origin transition starts. This method exposes the opening and closing 502 * windows as wrapped {@link UIComponent} to provide simplified interface to clients. 503 */ onStart( UIComponent.Transaction transaction, UIComponent origin, UIComponent closingApp, UIComponent openingApp)504 void onStart( 505 UIComponent.Transaction transaction, 506 UIComponent origin, 507 UIComponent closingApp, 508 UIComponent openingApp); 509 510 /** Called to update the transition frame. */ onProgress(float progress)511 void onProgress(float progress); 512 513 /** Called when the transition ended. */ onEnd(boolean finished)514 void onEnd(boolean finished); 515 } 516 } 517