Files
Lawnchair/systemUI/anim/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
T
Pun Butrach 11f7abcb66 chore: Cleanup, and hidden feature logged
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2026-01-10 20:48:25 +07:00

517 lines
19 KiB
Java

/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.animation;
import static android.view.WindowManager.TRANSIT_CHANGE;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.TransitionInfo;
import android.window.TransitionInfo.Change;
import android.window.WindowAnimationState;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.shared.TransitionUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
* and automatically attaches it to the transition leash before the transition starts.
*
* @hide
*/
public class OriginRemoteTransition extends IRemoteTransition.Stub {
private static final String TAG = "OriginRemoteTransition";
private static final long FINISH_ANIMATION_TIMEOUT_MS = 100;
private final Context mContext;
private final boolean mIsEntry;
private final UIComponent mOrigin;
private final TransitionPlayer mPlayer;
private final long mDuration;
private final Handler mHandler;
@Nullable private SurfaceControl.Transaction mStartTransaction;
@Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
@Nullable private UIComponent.Transaction mOriginTransaction;
@Nullable private ValueAnimator mAnimator;
@Nullable private SurfaceControl mOriginLeash;
private boolean mCancelled;
OriginRemoteTransition(
Context context,
boolean isEntry,
UIComponent origin,
TransitionPlayer player,
long duration,
Handler handler) {
mContext = context;
mIsEntry = isEntry;
mOrigin = origin;
mPlayer = player;
mDuration = duration;
mHandler = handler;
}
@Override
public void startAnimation(
IBinder token,
TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) {
logD("startAnimation - " + info);
mHandler.post(
() -> {
mStartTransaction = t;
mFinishCallback = finishCallback;
startAnimationInternal(info, /* states= */ null);
});
}
@Override
public void mergeAnimation(
IBinder transition,
TransitionInfo info,
SurfaceControl.Transaction t,
IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) {
logD("mergeAnimation - " + info);
cancel();
}
@Override
public void takeOverAnimation(
IBinder transition,
TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback,
WindowAnimationState[] states) {
logD("takeOverAnimation - info=" + info + ", states=" + Arrays.toString(states));
mHandler.post(
() -> {
mStartTransaction = t;
mFinishCallback = finishCallback;
startAnimationInternal(info, states);
});
}
@Override
public void onTransitionConsumed(IBinder transition, boolean aborted) {
logD("onTransitionConsumed - aborted: " + aborted);
cancel();
}
private void startAnimationInternal(
TransitionInfo info, @Nullable WindowAnimationState[] states) {
if (!prepareUIs(info)) {
logE("Unable to prepare UI!");
finishAnimation(/* finished= */ false);
return;
}
// Notify player that we are starting.
mPlayer.onStart(info, states, mStartTransaction, mOrigin, mOriginTransaction);
// Apply the initial transactions in case the player forgot to apply them.
mOriginTransaction.commit();
mStartTransaction.apply();
// Start the animator.
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mAnimator.setDuration(mDuration);
mAnimator.addListener(
new AnimatorListener() {
@Override
public void onAnimationStart(Animator a) {}
@Override
public void onAnimationEnd(Animator a) {
finishAnimation(/* finished= */ !mCancelled);
}
@Override
public void onAnimationCancel(Animator a) {
mCancelled = true;
}
@Override
public void onAnimationRepeat(Animator a) {}
});
mAnimator.addUpdateListener(
a -> {
mPlayer.onProgress((float) a.getAnimatedValue());
});
mAnimator.start();
}
private boolean prepareUIs(TransitionInfo info) {
if (info.getRootCount() == 0) {
logE("prepareUIs: no root leash!");
return false;
}
if (info.getRootCount() > 1) {
logE("prepareUIs: multi-display transition is not supported yet!");
return false;
}
if (info.getChanges().isEmpty()) {
logE("prepareUIs: no changes!");
return false;
}
SurfaceControl rootLeash = info.getRoot(0).getLeash();
int displayId = info.getChanges().get(0).getEndDisplayId();
Rect displayBounds = getDisplayBounds(displayId);
float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);
// Create the origin leash and add to the transition root leash.
mOriginLeash =
new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
// Create temporary transaction to build
final SurfaceControl.Transaction tmpTransaction = new SurfaceControl.Transaction();
tmpTransaction
.reparent(mOriginLeash, rootLeash)
.show(mOriginLeash)
.setCornerRadius(mOriginLeash, windowRadius)
.setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());
// Process surfaces
List<SurfaceControl> openingSurfaces = new ArrayList<>();
List<SurfaceControl> closingSurfaces = new ArrayList<>();
for (Change change : info.getChanges()) {
int mode = change.getMode();
SurfaceControl leash = change.getLeash();
// Reparent leash to the transition root.
tmpTransaction.reparent(leash, rootLeash);
if (TransitionUtil.isOpeningMode(mode)) {
openingSurfaces.add(change.getLeash());
// For opening surfaces, ending bounds are base bound. Apply corner radius if
// it's full screen.
Rect bounds = change.getEndAbsBounds();
if (displayBounds.equals(bounds)) {
tmpTransaction
.setCornerRadius(leash, windowRadius)
.setWindowCrop(leash, bounds.width(), bounds.height());
}
} else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) {
// TRANSIT_CHANGE refers to the closing window in predictive back animation.
closingSurfaces.add(change.getLeash());
// For closing surfaces, starting bounds are base bounds. Apply corner radius if
// it's full screen.
Rect bounds = change.getStartAbsBounds();
if (displayBounds.equals(bounds)) {
tmpTransaction
.setCornerRadius(leash, windowRadius)
.setWindowCrop(leash, bounds.width(), bounds.height());
}
}
}
if (openingSurfaces.isEmpty() && closingSurfaces.isEmpty()) {
logD("prepareUIs: no opening/closing surfaces available, nothing to prepare.");
return false;
}
// Set relative order:
// ---- App1 ----
// ---- origin ----
// ---- App2 ----
if (mIsEntry) {
if (!closingSurfaces.isEmpty()) {
tmpTransaction.setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1);
} else {
logW("Missing closing surface is entry transition");
}
if (!openingSurfaces.isEmpty()) {
tmpTransaction.setRelativeLayer(
openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
} else {
logW("Missing opening surface is entry transition");
}
} else {
if (!openingSurfaces.isEmpty()) {
tmpTransaction.setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1);
} else {
logW("Missing opening surface is exit transition");
}
if (!closingSurfaces.isEmpty()) {
tmpTransaction.setRelativeLayer(
closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
} else {
logW("Missing closing surface is exit transition");
}
}
mStartTransaction.merge(tmpTransaction);
// Attach origin UIComponent to origin leash.
mOriginTransaction = mOrigin.newTransaction();
mOriginTransaction.attachToTransitionLeash(
mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height());
return true;
}
private Rect getDisplayBounds(int displayId) {
DisplayManager dm = mContext.getSystemService(DisplayManager.class);
DisplayMetrics metrics = new DisplayMetrics();
dm.getDisplay(displayId).getMetrics(metrics);
return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
}
private void finishAnimation(boolean finished) {
logD("finishAnimation: finished=" + finished);
OneShotRunnable finishInternalRunnable = new OneShotRunnable(this::finishInternal);
Runnable timeoutRunnable =
() -> {
Log.w(TAG, "Timeout waiting for surface transaction!");
finishInternalRunnable.run();
};
Runnable committedRunnable =
() -> {
// Remove the timeout runnable.
mHandler.removeCallbacks(timeoutRunnable);
finishInternalRunnable.run();
};
if (mAnimator == null) {
// The transition didn't start. Ensure we apply the start transaction and report
// finish afterwards.
mStartTransaction
.addTransactionCommittedListener(mHandler::post, committedRunnable::run)
.apply();
// Call finishInternal() anyway after the timeout.
mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS);
return;
}
mAnimator = null;
// Notify client that we have ended.
mPlayer.onEnd(finished);
// Detach the origin from the transition leash and report finish after it's done.
mOriginTransaction
.detachFromTransitionLeash(mOrigin, mHandler::post, committedRunnable)
.commit();
// Call finishInternal() anyway after the timeout.
mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS);
}
private void finishInternal() {
logD("finishInternal");
if (mOriginLeash != null) {
// Release origin leash.
mOriginLeash.release();
mOriginLeash = null;
}
try {
mFinishCallback.onTransitionFinished(null, null);
} catch (RemoteException e) {
logE("Unable to report transition finish!", e);
}
mStartTransaction = null;
mOriginTransaction = null;
mFinishCallback = null;
}
public void cancel() {
logD("cancel()");
mHandler.post(
() -> {
if (mAnimator != null) {
mAnimator.cancel();
}
});
}
private static void logD(String msg) {
if (OriginTransitionSession.DEBUG) {
Log.d(TAG, msg);
}
}
private static void logW(String msg) {
Log.w(TAG, msg);
}
private static void logE(String msg) {
Log.e(TAG, msg);
}
private static void logE(String msg, Throwable e) {
Log.e(TAG, msg, e);
}
private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
List<SurfaceControl> surfaces = new ArrayList<>();
Rect maxBounds = new Rect();
for (Change change : info.getChanges()) {
int mode = change.getMode();
if (TransitionUtil.isOpeningMode(mode) == isOpening) {
surfaces.add(change.getLeash());
Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
maxBounds.union(bounds);
}
}
return new SurfaceUIComponent(
surfaces,
/* alpha= */ 1.0f,
/* visible= */ true,
/* bounds= */ maxBounds,
/* baseBounds= */ maxBounds);
}
private static void applyWindowAnimationStates(
TransitionInfo info,
@Nullable WindowAnimationState[] states,
UIComponent closingApp,
UIComponent openingApp) {
if (states == null) {
// Nothing to apply.
return;
}
// Calculate bounds.
Rect maxClosingBounds = new Rect();
Rect maxOpeningBounds = new Rect();
for (int i = 0; i < info.getChanges().size(); i++) {
Rect bound = getBounds(states[i]);
if (bound == null) {
continue;
}
int mode = info.getChanges().get(i).getMode();
if (TransitionUtil.isOpeningMode(mode)) {
maxOpeningBounds.union(bound);
} else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) {
// TRANSIT_CHANGE refers to the closing window in predictive back animation.
maxClosingBounds.union(bound);
}
}
// Intentionally use a new transaction instead of reusing the existing transaction since we
// want to apply window animation states first without committing any other pending changes
// in the existing transaction. The existing transaction is expected to be committed by the
// onStart() client callback together with client's custom transformation.
UIComponent.Transaction transaction = closingApp.newTransaction();
if (!maxClosingBounds.isEmpty()) {
logD("Applying closing window bounds: " + maxClosingBounds);
transaction.setBounds(closingApp, maxClosingBounds);
}
if (!maxOpeningBounds.isEmpty()) {
logD("Applying opening window bounds: " + maxOpeningBounds);
transaction.setBounds(openingApp, maxOpeningBounds);
}
transaction.commit();
}
@Nullable
private static Rect getBounds(@Nullable WindowAnimationState state) {
if (state == null || state.bounds == null) {
return null;
}
Rect out = new Rect();
state.bounds.roundOut(out);
return out;
}
/** A {@link Runnable} that will only run once. */
private static class OneShotRunnable implements Runnable {
private final AtomicBoolean mDone = new AtomicBoolean();
private final Runnable mRunnable;
OneShotRunnable(Runnable runnable) {
this.mRunnable = runnable;
}
@Override
public void run() {
if (!mDone.getAndSet(true)) {
mRunnable.run();
}
}
}
/**
* An interface that represents an origin transitions.
*
* @hide
*/
public interface TransitionPlayer {
/**
* Called when an origin transition starts. This method exposes the raw {@link
* TransitionInfo} so that clients can extract more information from it.
*
* <p>Note: if this transition is taking over a predictive back animation, the {@link
* WindowAnimationState} will be passed to this method. The concrete implementation is
* expected to apply the {@link WindowAnimationState} before continuing the transition.
*/
default void onStart(
TransitionInfo transitionInfo,
@Nullable WindowAnimationState[] states,
SurfaceControl.Transaction sfTransaction,
UIComponent origin,
UIComponent.Transaction uiTransaction) {
// Wrap transactions.
Transactions transactions =
new Transactions()
.registerTransactionForClass(origin.getClass(), uiTransaction)
.registerTransactionForClass(
SurfaceUIComponent.class,
new SurfaceUIComponent.Transaction(sfTransaction));
// Wrap surfaces.
UIComponent closingApp = wrapSurfaces(transitionInfo, /* isOpening= */ false);
UIComponent openingApp = wrapSurfaces(transitionInfo, /* isOpening= */ true);
// Restore the pending animation states coming from predictive back transition.
applyWindowAnimationStates(transitionInfo, states, closingApp, openingApp);
// Start.
onStart(transactions, origin, closingApp, openingApp);
}
/**
* Called when an origin transition starts. This method exposes the opening and closing
* windows as wrapped {@link UIComponent} to provide simplified interface to clients.
*/
void onStart(
UIComponent.Transaction transaction,
UIComponent origin,
UIComponent closingApp,
UIComponent openingApp);
/** Called to update the transition frame. */
void onProgress(float progress);
/** Called when the transition ended. */
void onEnd(boolean finished);
}
}