blob: f020bfa8cbc72e1f13315daec570db136a9de903 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.wm;
import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.TransitionFlags;
import static android.view.WindowManager.TransitionType;
import static android.view.WindowManager.transitTypeToString;
import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_TASK_LAUNCHING_BEHIND;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static android.window.TransitionInfo.FLAG_WILL_IME_SHOWN;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.IApplicationThread;
import android.content.pm.ActivityInfo;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ScreenCapture;
import android.window.TransitionInfo;
import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Represents a logical transition. This keeps track of all the changes associated with a logical
* WM state -> state transition.
* @see TransitionController
*
* In addition to tracking individual container changes, this also tracks ordering-changes (just
* on-top for now). However, since order is a "global" property, the mechanics of order-change
* detection/reporting is non-trivial when transitions are collecting in parallel. See
* {@link #collectOrderChanges} for more details.
*/
class Transition implements BLASTSyncEngine.TransactionReadyListener {
private static final String TAG = "Transition";
private static final String TRACE_NAME_PLAY_TRANSITION = "playing";
/** The default package for resources */
private static final String DEFAULT_PACKAGE = "android";
/** The transition has been created but isn't collecting yet. */
private static final int STATE_PENDING = -1;
/** The transition has been created and is collecting, but hasn't formally started. */
private static final int STATE_COLLECTING = 0;
/**
* The transition has formally started. It is still collecting but will stop once all
* participants are ready to animate (finished drawing).
*/
private static final int STATE_STARTED = 1;
/**
* This transition is currently playing its animation and can no longer collect or be changed.
*/
private static final int STATE_PLAYING = 2;
/**
* This transition is aborting or has aborted. No animation will play nor will anything get
* sent to the player.
*/
private static final int STATE_ABORT = 3;
/**
* This transition has finished playing successfully.
*/
private static final int STATE_FINISHED = 4;
@IntDef(prefix = { "STATE_" }, value = {
STATE_PENDING,
STATE_COLLECTING,
STATE_STARTED,
STATE_PLAYING,
STATE_ABORT,
STATE_FINISHED
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionState {}
final @TransitionType int mType;
private int mSyncId = -1;
private @TransitionFlags int mFlags;
private final TransitionController mController;
private final BLASTSyncEngine mSyncEngine;
private final Token mToken;
private @Nullable ActivityRecord mPipActivity;
/** Only use for clean-up after binder death! */
private SurfaceControl.Transaction mStartTransaction = null;
private SurfaceControl.Transaction mFinishTransaction = null;
/** Used for failsafe clean-up to prevent leaks due to misbehaving player impls. */
private SurfaceControl.Transaction mCleanupTransaction = null;
/**
* Contains change infos for both participants and all remote-animatable ancestors. The
* ancestors can be the promotion candidates so their start-states need to be captured.
* @see #getAnimatableParent
*/
final ArrayMap<WindowContainer, ChangeInfo> mChanges = new ArrayMap<>();
/** The collected participants in the transition. */
final ArraySet<WindowContainer> mParticipants = new ArraySet<>();
/** The final animation targets derived from participants after promotion. */
ArrayList<ChangeInfo> mTargets;
/** The displays that this transition is running on. */
private final ArrayList<DisplayContent> mTargetDisplays = new ArrayList<>();
/**
* The (non alwaysOnTop) tasks which were on-top of their display before the transition. If
* tasks are nested, all the tasks that are parents of the on-top task are also included.
*/
private final ArrayList<Task> mOnTopTasksStart = new ArrayList<>();
/**
* The (non alwaysOnTop) tasks which were on-top of their display when this transition became
* ready (via setReady, not animation-ready).
*/
private final ArrayList<Task> mOnTopTasksAtReady = new ArrayList<>();
/**
* Set of participating windowtokens (activity/wallpaper) which are visible at the end of
* the transition animation.
*/
private final ArraySet<WindowToken> mVisibleAtTransitionEndTokens = new ArraySet<>();
/**
* Set of transient activities (lifecycle initially tied to this transition) and their
* restore-below tasks.
*/
private ArrayMap<ActivityRecord, Task> mTransientLaunches = null;
/**
* The tasks that may be occluded by the transient activity. Assume the task stack is
* [Home, A(opaque), B(opaque), C(translucent)] (bottom to top), then A is the restore-below
* task, and [B, C] are the transient-hide tasks.
*/
private ArrayList<Task> mTransientHideTasks;
/** Custom activity-level animation options and callbacks. */
private TransitionInfo.AnimationOptions mOverrideOptions;
private IRemoteCallback mClientAnimationStartCallback = null;
private IRemoteCallback mClientAnimationFinishCallback = null;
private @TransitionState int mState = STATE_PENDING;
private final ReadyTrackerOld mReadyTrackerOld = new ReadyTrackerOld();
final ReadyTracker mReadyTracker = new ReadyTracker(this);
private int mRecentsDisplayId = INVALID_DISPLAY;
/** The delay for light bar appearance animation. */
long mStatusBarTransitionDelay;
/** @see #setCanPipOnFinish */
private boolean mCanPipOnFinish = true;
private boolean mIsSeamlessRotation = false;
private IContainerFreezer mContainerFreezer = null;
/**
* {@code true} if some other operation may have caused the originally-recorded state (in
* mChanges) to be dirty. This is usually due to finishTransition being called mid-collect;
* and, the reason that finish can alter the "start" state of other transitions is because
* setVisible(false) is deferred until then.
* Instead of adding this conditional, we could re-check always; but, this situation isn't
* common so it'd be wasted work.
*/
boolean mPriorVisibilityMightBeDirty = false;
final TransitionController.Logger mLogger = new TransitionController.Logger();
/** Whether this transition was forced to play early (eg for a SLEEP signal). */
private boolean mForcePlaying = false;
/**
* {@code false} if this transition runs purely in WMCore (meaning Shell is completely unaware
* of it). Currently, this happens before the display is ready since nothing can be seen yet.
*/
boolean mIsPlayerEnabled = true;
/** This transition doesn't run in parallel. */
static final int PARALLEL_TYPE_NONE = 0;
/** Any 2 transitions of this type can run in parallel with each other. Used for testing. */
static final int PARALLEL_TYPE_MUTUAL = 1;
/** This is a recents transition. */
static final int PARALLEL_TYPE_RECENTS = 2;
@IntDef(prefix = { "PARALLEL_TYPE_" }, value = {
PARALLEL_TYPE_NONE,
PARALLEL_TYPE_MUTUAL,
PARALLEL_TYPE_RECENTS
})
@Retention(RetentionPolicy.SOURCE)
@interface ParallelType {}
/**
* What category of parallel-collect support this transition has. The value of this is used
* by {@link TransitionController} to determine which transitions can collect in parallel. If
* a transition can collect in parallel, it means that it will start collecting as soon as the
* prior collecting transition is {@link #isPopulated}. This is a shortcut for supporting
* a couple specific situations before we have full-fledged support for parallel transitions.
*/
@ParallelType int mParallelCollectType = PARALLEL_TYPE_NONE;
/**
* A "Track" is a set of animations which must cooperate with each other to play smoothly. If
* animations can play independently of each other, then they can be in different tracks. If
* a transition must cooperate with transitions in >1 other track, then it must be marked
* FLAG_SYNC and it will end-up flushing all animations before it starts.
*/
int mAnimationTrack = 0;
Transition(@TransitionType int type, @TransitionFlags int flags,
TransitionController controller, BLASTSyncEngine syncEngine) {
mType = type;
mFlags = flags;
mController = controller;
mSyncEngine = syncEngine;
mToken = new Token(this);
mLogger.mCreateWallTimeMs = System.currentTimeMillis();
mLogger.mCreateTimeNs = SystemClock.elapsedRealtimeNanos();
}
@Nullable
static Transition fromBinder(@Nullable IBinder token) {
if (token == null) return null;
try {
return ((Token) token).mTransition.get();
} catch (ClassCastException e) {
Slog.w(TAG, "Invalid transition token: " + token, e);
return null;
}
}
@NonNull
IBinder getToken() {
return mToken;
}
void addFlag(int flag) {
mFlags |= flag;
}
void calcParallelCollectType(WindowContainerTransaction wct) {
for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
final WindowContainerTransaction.HierarchyOp hop = wct.getHierarchyOps().get(i);
if (hop.getType() != HIERARCHY_OP_TYPE_PENDING_INTENT) continue;
final Bundle b = hop.getLaunchOptions();
if (b == null || b.isEmpty()) continue;
final boolean transientLaunch = b.getBoolean(ActivityOptions.KEY_TRANSIENT_LAUNCH);
if (transientLaunch) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Starting a Recents transition which can be parallel.");
mParallelCollectType = PARALLEL_TYPE_RECENTS;
}
}
}
/** Records an activity as transient-launch. This activity must be already collected. */
void setTransientLaunch(@NonNull ActivityRecord activity, @Nullable Task restoreBelow) {
if (mTransientLaunches == null) {
mTransientLaunches = new ArrayMap<>();
mTransientHideTasks = new ArrayList<>();
}
mTransientLaunches.put(activity, restoreBelow);
setTransientLaunchToChanges(activity);
final Task transientRootTask = activity.getRootTask();
final WindowContainer<?> parent = restoreBelow != null ? restoreBelow.getParent()
: (transientRootTask != null ? transientRootTask.getParent() : null);
if (parent != null) {
// Collect all visible tasks which can be occluded by the transient activity to
// make sure they are in the participants so their visibilities can be updated when
// finishing transition.
parent.forAllTasks(t -> {
// Skip transient-launch task
if (t == transientRootTask) return false;
if (t.isVisibleRequested() && !t.isAlwaysOnTop()) {
if (t.isRootTask()) {
mTransientHideTasks.add(t);
}
if (t.isLeafTask()) {
collect(t);
}
}
return restoreBelow != null
// Stop at the restoreBelow task
? t == restoreBelow
// Or stop at the last visible task if no restore-below (new task)
: (t.isRootTask() && t.fillsParent());
});
// Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks,
// so ChangeInfo#hasChanged() can return true to report the transition info.
for (int i = mChanges.size() - 1; i >= 0; --i) {
updateTransientFlags(mChanges.valueAt(i));
}
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as "
+ "transient-launch", mSyncId, activity);
}
/** @return whether `wc` is a descendent of a transient-hide window. */
boolean isInTransientHide(@NonNull WindowContainer wc) {
if (mTransientHideTasks == null) return false;
for (int i = mTransientHideTasks.size() - 1; i >= 0; --i) {
final Task task = mTransientHideTasks.get(i);
if (wc == task || wc.isDescendantOf(task)) {
return true;
}
}
return false;
}
/** Returns {@code true} if the task should keep visible if this is a transient transition. */
boolean isTransientVisible(@NonNull Task task) {
if (mTransientLaunches == null) return false;
int occludedCount = 0;
final int numTransient = mTransientLaunches.size();
for (int i = numTransient - 1; i >= 0; --i) {
final Task transientRoot = mTransientLaunches.keyAt(i).getRootTask();
if (transientRoot == null) continue;
final WindowContainer<?> rootParent = transientRoot.getParent();
if (rootParent == null || rootParent.getTopChild() == transientRoot) continue;
final ActivityRecord topOpaque = mController.mAtm.mTaskSupervisor.mOpaqueActivityHelper
.getOpaqueActivity(rootParent, true /* ignoringKeyguard */);
if (transientRoot.compareTo(topOpaque.getRootTask()) < 0) {
occludedCount++;
}
}
if (occludedCount == numTransient) {
for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
if (mTransientLaunches.keyAt(i).isDescendantOf(task)) {
// Keep transient activity visible until transition finished, so it won't pause
// with transient-hide tasks that may delay resuming the next top.
return true;
}
}
// Let transient-hide activities pause before transition is finished.
return false;
}
return isInTransientHide(task);
}
boolean canApplyDim(@NonNull Task task) {
if (mTransientLaunches == null) return true;
final Dimmer dimmer = task.getDimmer();
if (dimmer == null) {
return false;
}
if (dimmer.getHost().asTask() != null) {
// Always allow to dim if the host only affects its task.
return true;
}
// The dimmer host of a translucent task can be a display, then it is not in transient-hide.
for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
// The transient task is usually the task of recents/home activity.
final Task transientTask = mTransientLaunches.keyAt(i).getTask();
if (transientTask != null && transientTask.canAffectSystemUiFlags()) {
// It usually means that the recents animation has moved the transient-hide task
// an noticeable distance, then the display level dimmer should not show.
return false;
}
}
return true;
}
boolean hasTransientLaunch() {
return mTransientLaunches != null && !mTransientLaunches.isEmpty();
}
boolean isTransientLaunch(@NonNull ActivityRecord activity) {
return mTransientLaunches != null && mTransientLaunches.containsKey(activity);
}
Task getTransientLaunchRestoreTarget(@NonNull WindowContainer container) {
if (mTransientLaunches == null) return null;
for (int i = 0; i < mTransientLaunches.size(); ++i) {
if (mTransientLaunches.keyAt(i).isDescendantOf(container)) {
return mTransientLaunches.valueAt(i);
}
}
return null;
}
boolean isOnDisplay(@NonNull DisplayContent dc) {
return mTargetDisplays.contains(dc);
}
/** Set a transition to be a seamless-rotation. */
void setSeamlessRotation(@NonNull WindowContainer wc) {
final ChangeInfo info = mChanges.get(wc);
if (info == null) return;
info.mFlags = info.mFlags | ChangeInfo.FLAG_SEAMLESS_ROTATION;
onSeamlessRotating(wc.getDisplayContent());
}
/**
* Called when it's been determined that this is transition is a seamless rotation. This should
* be called before any WM changes have happened.
*/
void onSeamlessRotating(@NonNull DisplayContent dc) {
// Don't need to do anything special if everything is using BLAST sync already.
if (mSyncEngine.getSyncSet(mSyncId).mSyncMethod == BLASTSyncEngine.METHOD_BLAST) return;
if (mContainerFreezer == null) {
mContainerFreezer = new ScreenshotFreezer();
}
final WindowState top = dc.getDisplayPolicy().getTopFullscreenOpaqueWindow();
if (top != null) {
mIsSeamlessRotation = true;
top.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Override sync-method for %s "
+ "because seamless rotating", top.getName());
}
}
/**
* Set the pip-able activity participating in this transition.
* @param pipActivity activity about to enter pip
*/
void setPipActivity(@Nullable ActivityRecord pipActivity) {
mPipActivity = pipActivity;
}
/**
* @return pip-able activity participating in this transition.
*/
@Nullable ActivityRecord getPipActivity() {
return mPipActivity;
}
/**
* Only set flag to the parent tasks and activity itself.
*/
private void setTransientLaunchToChanges(@NonNull WindowContainer wc) {
for (WindowContainer curr = wc; curr != null && mChanges.containsKey(curr);
curr = curr.getParent()) {
if (curr.asTask() == null && curr.asActivityRecord() == null) {
return;
}
final ChangeInfo info = mChanges.get(curr);
info.mFlags = info.mFlags | ChangeInfo.FLAG_TRANSIENT_LAUNCH;
}
}
/** Only for testing. */
void setContainerFreezer(IContainerFreezer freezer) {
mContainerFreezer = freezer;
}
@TransitionState
int getState() {
return mState;
}
int getSyncId() {
return mSyncId;
}
@TransitionFlags
int getFlags() {
return mFlags;
}
@VisibleForTesting
SurfaceControl.Transaction getStartTransaction() {
return mStartTransaction;
}
@VisibleForTesting
SurfaceControl.Transaction getFinishTransaction() {
return mFinishTransaction;
}
boolean isPending() {
return mState == STATE_PENDING;
}
boolean isCollecting() {
return mState == STATE_COLLECTING || mState == STATE_STARTED;
}
boolean isAborted() {
return mState == STATE_ABORT;
}
boolean isStarted() {
return mState == STATE_STARTED;
}
boolean isPlaying() {
return mState == STATE_PLAYING;
}
boolean isFinished() {
return mState == STATE_FINISHED;
}
/** Starts collecting phase. Once this starts, all relevant surface operations are sync. */
void startCollecting(long timeoutMs) {
if (mState != STATE_PENDING) {
throw new IllegalStateException("Attempting to re-use a transition");
}
mState = STATE_COLLECTING;
mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG,
mParallelCollectType != PARALLEL_TYPE_NONE);
mSyncEngine.setSyncMethod(mSyncId, TransitionController.SYNC_METHOD);
mLogger.mSyncId = mSyncId;
mLogger.mCollectTimeNs = SystemClock.elapsedRealtimeNanos();
}
/**
* Formally starts the transition. Participants can be collected before this is started,
* but this won't consider itself ready until started -- even if all the participants have
* drawn.
*/
void start() {
if (mState < STATE_COLLECTING) {
throw new IllegalStateException("Can't start Transition which isn't collecting.");
} else if (mState >= STATE_STARTED) {
Slog.w(TAG, "Transition already started id=" + mSyncId + " state=" + mState);
// The transition may be aborted (STATE_ABORT) or timed out (STATE_PLAYING by
// SyncGroup#finishNow), so do not revert the state to STATE_STARTED.
return;
}
mState = STATE_STARTED;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Starting Transition %d",
mSyncId);
applyReady();
mLogger.mStartTimeNs = SystemClock.elapsedRealtimeNanos();
mController.updateAnimatingState();
}
/**
* Adds wc to set of WindowContainers participating in this transition.
*/
void collect(@NonNull WindowContainer wc) {
if (mState < STATE_COLLECTING) {
throw new IllegalStateException("Transition hasn't started collecting.");
}
if (!isCollecting()) {
// Too late, transition already started playing, so don't collect.
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s",
mSyncId, wc);
// "snapshot" all parents (as potential promotion targets). Do this before checking
// if this is already a participant in case it has since been re-parented.
for (WindowContainer<?> curr = getAnimatableParent(wc);
curr != null && !mChanges.containsKey(curr);
curr = getAnimatableParent(curr)) {
final ChangeInfo info = new ChangeInfo(curr);
updateTransientFlags(info);
mChanges.put(curr, info);
if (isReadyGroup(curr)) {
mReadyTrackerOld.addGroup(curr);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
+ " Transition %d with root=%s", mSyncId, curr);
}
}
if (mParticipants.contains(wc)) return;
// Transient-hide may be hidden later, so no need to request redraw.
if (!isInTransientHide(wc)) {
mSyncEngine.addToSyncSet(mSyncId, wc);
}
if (wc.asWindowToken() != null && wc.asWindowToken().mRoundedCornerOverlay) {
// Only need to sync the transaction (SyncSet) without ChangeInfo because cutout and
// rounded corner overlay never need animations. Especially their surfaces may be put
// in root (null, see WindowToken#makeSurface()) that cannot reparent.
return;
}
ChangeInfo info = mChanges.get(wc);
if (info == null) {
info = new ChangeInfo(wc);
updateTransientFlags(info);
mChanges.put(wc, info);
}
mParticipants.add(wc);
recordDisplay(wc.getDisplayContent());
if (info.mShowWallpaper) {
// Collect the wallpaper token (for isWallpaper(wc)) so it is part of the sync set.
final List<WindowState> wallpapers =
wc.getDisplayContent().mWallpaperController.getAllTopWallpapers();
for (int i = wallpapers.size() - 1; i >= 0; i--) {
WindowState wallpaper = wallpapers.get(i);
collect(wallpaper.mToken);
}
}
}
private void updateTransientFlags(@NonNull ChangeInfo info) {
final WindowContainer<?> wc = info.mContainer;
// Only look at tasks, taskfragments, or activities
if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) return;
if (!isInTransientHide(wc)) return;
info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
}
private void recordDisplay(DisplayContent dc) {
if (dc == null || mTargetDisplays.contains(dc)) return;
mTargetDisplays.add(dc);
addOnTopTasks(dc, mOnTopTasksStart);
// Handle the case {transition.start(); applyTransaction(wct);} that the animating state
// is set before collecting participants.
if (mController.isAnimating()) {
dc.enableHighPerfTransition(true);
}
}
/**
* Records information about the initial task order. This does NOT collect anything. Call this
* before any ordering changes *could* occur, but it is not known yet if it will occur.
*/
void recordTaskOrder(WindowContainer from) {
recordDisplay(from.getDisplayContent());
}
/** Adds the top non-alwaysOnTop tasks within `task` to `out`. */
private static void addOnTopTasks(Task task, ArrayList<Task> out) {
for (int i = task.getChildCount() - 1; i >= 0; --i) {
final Task child = task.getChildAt(i).asTask();
if (child == null) return;
if (child.getWindowConfiguration().isAlwaysOnTop()) continue;
out.add(child);
addOnTopTasks(child, out);
break;
}
}
/** Get the top non-alwaysOnTop leaf task on the display `dc`. */
private static void addOnTopTasks(DisplayContent dc, ArrayList<Task> out) {
final Task topNotAlwaysOnTop = dc.getRootTask(
t -> !t.getWindowConfiguration().isAlwaysOnTop());
if (topNotAlwaysOnTop == null) return;
out.add(topNotAlwaysOnTop);
addOnTopTasks(topNotAlwaysOnTop, out);
}
/**
* Records wc as changing its state of existence during this transition. For example, a new
* task is considered an existence change while moving a task to front is not. wc is added
* to the collection set. Note: Existence is NOT a promotable characteristic.
*
* This must be explicitly recorded because there are o number of situations where the actual
* hierarchy operations don't align with the intent (eg. re-using a task with a new activity
* or waiting until after the animation to close).
*/
void collectExistenceChange(@NonNull WindowContainer wc) {
if (mState >= STATE_PLAYING) {
// Too late to collect. Don't check too-early here since `collect` will check that.
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Existence Changed in transition %d:"
+ " %s", mSyncId, wc);
collect(wc);
mChanges.get(wc).mExistenceChanged = true;
}
/**
* Records that a particular container is changing visibly (ie. something about it is changing
* while it remains visible). This only effects windows that are already in the collecting
* transition.
*/
void collectVisibleChange(WindowContainer wc) {
if (mSyncEngine.getSyncSet(mSyncId).mSyncMethod == BLASTSyncEngine.METHOD_BLAST) {
// All windows are synced already.
return;
}
if (wc.mDisplayContent == null || !isInTransition(wc)) return;
if (!wc.mDisplayContent.getDisplayPolicy().isScreenOnFully()
|| wc.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF) {
mFlags |= WindowManager.TRANSIT_FLAG_INVISIBLE;
return;
}
// Activity doesn't need to capture snapshot if the starting window has associated to task.
if (wc.asActivityRecord() != null) {
final ActivityRecord activityRecord = wc.asActivityRecord();
if (activityRecord.mStartingData != null
&& activityRecord.mStartingData.mAssociatedTask != null) {
return;
}
}
if (mContainerFreezer == null) {
mContainerFreezer = new ScreenshotFreezer();
}
Transition.ChangeInfo change = mChanges.get(wc);
if (change == null || !change.mVisible || !wc.isVisibleRequested()) return;
// Note: many more tests have already been done by caller.
mContainerFreezer.freeze(wc, change.mAbsoluteBounds);
}
/**
* Records that a particular container has been reparented. This only effects windows that have
* already been collected in the transition. This should be called before reparenting because
* the old parent may be removed during reparenting, for example:
* {@link Task#shouldRemoveSelfOnLastChildRemoval}
*/
void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) {
if (!mChanges.containsKey(wc)) {
// #collectReparentChange() will be called when the window is reparented. Skip if it is
// a window that has not been collected, which means we don't care about this window for
// the current transition.
return;
}
final ChangeInfo change = mChanges.get(wc);
// Use the current common ancestor if there are multiple reparent, and the original parent
// has been detached. Otherwise, use the original parent before the transition.
final WindowContainer prevParent =
change.mStartParent == null || change.mStartParent.isAttached()
? change.mStartParent
: change.mCommonAncestor;
if (prevParent == null || !prevParent.isAttached()) {
Slog.w(TAG, "Trying to collect reparenting of a window after the previous parent has"
+ " been detached: " + wc);
return;
}
if (prevParent == newParent) {
Slog.w(TAG, "Trying to collect reparenting of a window that has not been reparented: "
+ wc);
return;
}
if (!newParent.isAttached()) {
Slog.w(TAG, "Trying to collect reparenting of a window that is not attached after"
+ " reparenting: " + wc);
return;
}
WindowContainer ancestor = newParent;
while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
ancestor = ancestor.getParent();
}
change.mCommonAncestor = ancestor;
}
/**
* @return {@code true} if `wc` is a participant or is a descendant of one.
*/
boolean isInTransition(WindowContainer wc) {
for (WindowContainer p = wc; p != null; p = p.getParent()) {
if (mParticipants.contains(p)) return true;
}
return false;
}
/**
* Specifies configuration change explicitly for the window container, so it can be chosen as
* transition target. This is usually used with transition mode
* {@link android.view.WindowManager#TRANSIT_CHANGE}.
*/
void setKnownConfigChanges(WindowContainer<?> wc, @ActivityInfo.Config int changes) {
final ChangeInfo changeInfo = mChanges.get(wc);
if (changeInfo != null) {
changeInfo.mKnownConfigChanges = changes;
}
}
private void sendRemoteCallback(@Nullable IRemoteCallback callback) {
if (callback == null) return;
mController.mAtm.mH.sendMessage(PooledLambda.obtainMessage(cb -> {
try {
cb.sendResult(null);
} catch (RemoteException e) { }
}, callback));
}
/**
* Set animation options for collecting transition by ActivityRecord.
* @param options AnimationOptions captured from ActivityOptions
*/
void setOverrideAnimation(TransitionInfo.AnimationOptions options,
@Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
if (!isCollecting()) return;
mOverrideOptions = options;
sendRemoteCallback(mClientAnimationStartCallback);
mClientAnimationStartCallback = startCallback;
mClientAnimationFinishCallback = finishCallback;
}
/**
* Call this when all known changes related to this transition have been applied. Until
* all participants have finished drawing, the transition can still collect participants.
*
* If this is called before the transition is started, it will be deferred until start.
*
* @param wc A reference point to determine which ready-group to update. For now, each display
* has its own ready-group, so this is used to look-up which display to mark ready.
* The transition will wait for all groups to be ready.
*/
void setReady(WindowContainer wc, boolean ready) {
if (!isCollecting() || mSyncId < 0) return;
mReadyTrackerOld.setReadyFrom(wc, ready);
applyReady();
}
private void applyReady() {
if (mState < STATE_STARTED) return;
final boolean ready;
if (mController.useFullReadyTracking()) {
ready = mReadyTracker.isReady();
} else {
ready = mReadyTrackerOld.allReady();
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Set transition ready=%b %d", ready, mSyncId);
boolean changed = mSyncEngine.setReady(mSyncId, ready);
if (changed && ready) {
mLogger.mReadyTimeNs = SystemClock.elapsedRealtimeNanos();
mOnTopTasksAtReady.clear();
for (int i = 0; i < mTargetDisplays.size(); ++i) {
addOnTopTasks(mTargetDisplays.get(i), mOnTopTasksAtReady);
}
mController.onTransitionPopulated(this);
}
}
/**
* Sets all possible ready groups to ready.
* @see ReadyTrackerOld#setAllReady
*/
void setAllReady() {
if (!isCollecting() || mSyncId < 0) return;
mReadyTrackerOld.setAllReady();
applyReady();
}
@VisibleForTesting
boolean allReady() {
return mReadyTrackerOld.allReady();
}
/** This transition has all of its expected participants. */
boolean isPopulated() {
return mState >= STATE_STARTED && mReadyTrackerOld.allReady();
}
/**
* Build a transaction that "resets" all the re-parenting and layer changes. This is
* intended to be applied at the end of the transition but before the finish callback. This
* needs to be passed/applied in shell because until finish is called, shell owns the surfaces.
* Additionally, this gives shell the ability to better deal with merged transitions.
*/
private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
final Point tmpPos = new Point();
// usually only size 1
final ArraySet<DisplayContent> displays = new ArraySet<>();
for (int i = mTargets.size() - 1; i >= 0; --i) {
final WindowContainer target = mTargets.get(i).mContainer;
if (target.getParent() != null) {
final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
final SurfaceControl origParent = getOrigParentSurface(target);
// Ensure surfaceControls are re-parented back into the hierarchy.
t.reparent(targetLeash, origParent);
t.setLayer(targetLeash, target.getLastLayer());
target.getRelativePosition(tmpPos);
t.setPosition(targetLeash, tmpPos.x, tmpPos.y);
// No need to clip the display in case seeing the clipped content when during the
// display rotation. No need to clip activities because they rely on clipping on
// task layers.
if (target.asTaskFragment() == null) {
t.setCrop(targetLeash, null /* crop */);
} else {
// Crop to the resolved override bounds.
final Rect clipRect = target.getResolvedOverrideBounds();
t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height());
}
t.setCornerRadius(targetLeash, 0);
t.setShadowRadius(targetLeash, 0);
t.setMatrix(targetLeash, 1, 0, 0, 1);
t.setAlpha(targetLeash, 1);
// The bounds sent to the transition is always a real bounds. This means we lose
// information about "null" bounds (inheriting from parent). Core will fix-up
// non-organized window surface bounds; however, since Core can't touch organized
// surfaces, add the "inherit from parent" restoration here.
if (target.isOrganized() && target.matchParentBounds()) {
t.setWindowCrop(targetLeash, -1, -1);
}
displays.add(target.getDisplayContent());
}
}
// Remove screenshot layers if necessary
if (mContainerFreezer != null) {
mContainerFreezer.cleanUp(t);
}
// Need to update layers on involved displays since they were all paused while
// the animation played. This puts the layers back into the correct order.
for (int i = displays.size() - 1; i >= 0; --i) {
if (displays.valueAt(i) == null) continue;
updateDisplayLayers(displays.valueAt(i), t);
}
for (int i = 0; i < info.getRootCount(); ++i) {
t.reparent(info.getRoot(i).getLeash(), null);
}
}
private static void updateDisplayLayers(DisplayContent dc, SurfaceControl.Transaction t) {
dc.mTransitionController.mBuildingFinishLayers = true;
try {
dc.assignChildLayers(t);
} finally {
dc.mTransitionController.mBuildingFinishLayers = false;
}
}
/**
* Build a transaction that cleans-up transition-only surfaces (transition root and snapshots).
* This will ALWAYS be applied on transition finish just in-case
*/
private static void buildCleanupTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change c = info.getChanges().get(i);
if (c.getSnapshot() != null) {
t.reparent(c.getSnapshot(), null);
}
// The fixed transform hint was set in DisplayContent#applyRotation(). Make sure to
// clear the hint in case the start transaction is not applied.
if (c.hasFlags(FLAG_IS_DISPLAY) && c.getStartRotation() != c.getEndRotation()
&& c.getContainer() != null) {
t.unsetFixedTransformHint(WindowContainer.fromBinder(c.getContainer().asBinder())
.asDisplayContent().mSurfaceControl);
}
}
for (int i = info.getRootCount() - 1; i >= 0; --i) {
final SurfaceControl leash = info.getRoot(i).getLeash();
if (leash == null) continue;
t.reparent(leash, null);
}
}
/**
* Set whether this transition can start a pip-enter transition when finished. This is usually
* true, but gets set to false when recents decides that it wants to finish its animation but
* not actually finish its animation (yeah...).
*/
void setCanPipOnFinish(boolean canPipOnFinish) {
mCanPipOnFinish = canPipOnFinish;
}
private boolean didCommitTransientLaunch() {
if (mTransientLaunches == null) return false;
for (int j = 0; j < mTransientLaunches.size(); ++j) {
if (mTransientLaunches.keyAt(j).isVisibleRequested()) {
return true;
}
}
return false;
}
/**
* Check if pip-entry is possible after finishing and enter-pip if it is.
*
* @return true if we are *guaranteed* to enter-pip. This means we return false if there's
* a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
*/
private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
return false;
}
final ActivityRecord resuming = getVisibleTransientLaunch(ar.getTaskDisplayArea());
if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) {
// force enable pip-on-task-switch now that we've committed to actually launching
// to the transient activity.
ar.supportsEnterPipOnTaskSwitch = true;
}
// Make sure this activity can enter pip under the current circumstances.
// `enterPictureInPicture` internally checks, but with beforeStopping=false which
// is specifically for non-auto-enter.
if (!ar.checkEnterPictureInPictureState("enterPictureInPictureMode",
true /* beforeStopping */)) {
return false;
}
final int prevMode = ar.getTask().getWindowingMode();
final boolean inPip = mController.mAtm.enterPictureInPictureMode(ar,
ar.pictureInPictureArgs, false /* fromClient */, true /* isAutoEnter */);
final int currentMode = ar.getTask().getWindowingMode();
if (prevMode == WINDOWING_MODE_FULLSCREEN && currentMode == WINDOWING_MODE_PINNED
&& mTransientLaunches != null
&& ar.mDisplayContent.hasTopFixedRotationLaunchingApp()) {
// There will be a display configuration change after finishing this transition.
// Skip dispatching the change for PiP task to avoid its activity drawing for the
// intermediate state which will cause flickering. The final PiP bounds in new
// rotation will be applied by PipTransition.
ar.mDisplayContent.mPinnedTaskController.setEnterPipTransaction(null);
}
return inPip;
}
// Legacy pip-entry (not via isAutoEnterEnabled).
if ((!ar.getTask().isVisibleRequested() || didCommitTransientLaunch())
&& ar.supportsPictureInPicture()) {
// force enable pip-on-task-switch now that we've committed to actually launching to the
// transient activity, and then recalculate whether we can attempt pip.
ar.supportsEnterPipOnTaskSwitch = true;
}
try {
// If not going auto-pip, the activity should be paused with user-leaving.
mController.mAtm.mTaskSupervisor.mUserLeaving = true;
ar.getTaskFragment().startPausing(false /* uiSleeping */, resuming, "finishTransition");
} finally {
mController.mAtm.mTaskSupervisor.mUserLeaving = false;
}
// Return false anyway because there's no guarantee that the app will enter pip.
return false;
}
/**
* The transition has finished animating and is ready to finalize WM state. This should not
* be called directly; use {@link TransitionController#finishTransition} instead.
*/
void finishTransition() {
if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) {
asyncTraceEnd(System.identityHashCode(this));
}
mLogger.mFinishTimeNs = SystemClock.elapsedRealtimeNanos();
mController.mLoggerHandler.post(mLogger::logOnFinish);
mController.mTransitionTracer.logFinishedTransition(this);
// Close the transactions now. They were originally copied to Shell in case we needed to
// apply them due to a remote failure. Since we don't need to apply them anymore, free them
// immediately.
if (mStartTransaction != null) mStartTransaction.close();
if (mFinishTransaction != null) mFinishTransaction.close();
mStartTransaction = mFinishTransaction = null;
if (mCleanupTransaction != null) {
mCleanupTransaction.apply();
mCleanupTransaction = null;
}
if (mState < STATE_PLAYING) {
throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId);
}
mController.mFinishingTransition = this;
if (mTransientHideTasks != null && !mTransientHideTasks.isEmpty()) {
// The transient hide tasks could be occluded now, e.g. returning to home. So trigger
// the update to make the activities in the tasks invisible-requested, then the next
// step can continue to commit the visibility.
mController.mAtm.mRootWindowContainer.ensureActivitiesVisible(null /* starting */,
0 /* configChanges */, true /* preserveWindows */);
// Record all the now-hiding activities so that they are committed. Just use
// mParticipants because we can avoid a new list this way.
for (int i = 0; i < mTransientHideTasks.size(); ++i) {
final Task rootTask = mTransientHideTasks.get(i);
rootTask.forAllActivities(r -> {
// Only check leaf-tasks that were collected
if (!mParticipants.contains(r.getTask())) return;
if (rootTask.isVisibleRequested()) {
// This transient-hide didn't hide, so don't commit anything (otherwise we
// could prematurely commit invisible on unrelated activities). To be safe,
// though, notify the controller to prevent degenerate cases.
if (!r.isVisibleRequested()) {
mController.mValidateCommitVis.add(r);
} else {
// Make sure onAppTransitionFinished can be notified.
mParticipants.add(r);
}
return;
}
// This did hide: commit immediately so that other transitions know about it.
mParticipants.add(r);
});
}
}
boolean hasParticipatedDisplay = false;
boolean hasVisibleTransientLaunch = false;
boolean enterAutoPip = false;
boolean committedSomeInvisible = false;
// Commit all going-invisible containers
for (int i = 0; i < mParticipants.size(); ++i) {
final WindowContainer<?> participant = mParticipants.valueAt(i);
final ActivityRecord ar = participant.asActivityRecord();
if (ar != null) {
final Task task = ar.getTask();
if (task == null) continue;
boolean visibleAtTransitionEnd = mVisibleAtTransitionEndTokens.contains(ar);
// visibleAtTransitionEnd is used to guard against pre-maturely committing
// invisible on a window which is actually hidden by a later transition and not this
// one. However, for a transient launch, we can't use this mechanism because the
// visibility is determined at finish. Instead, use a different heuristic: don't
// commit invisible if the window is already in a later transition. That later
// transition will then handle the commit.
if (isTransientLaunch(ar) && !ar.isVisibleRequested()
&& mController.inCollectingTransition(ar)) {
visibleAtTransitionEnd = true;
}
// We need both the expected visibility AND current requested-visibility to be
// false. If it is expected-visible but not currently visible, it means that
// another animation is queued-up to animate this to invisibility, so we can't
// remove the surfaces yet. If it is currently visible, but not expected-visible,
// then doing commitVisibility here would actually be out-of-order and leave the
// activity in a bad state.
// TODO (b/243755838) Create a screen off transition to correct the visible status
// of activities.
final boolean isScreenOff = ar.mDisplayContent == null
|| ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
final boolean commitVisibility = !checkEnterPipOnFinish(ar);
// Avoid commit visibility if entering pip or else we will get a sudden
// "flash" / surface going invisible for a split second.
if (commitVisibility) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Commit activity becoming invisible: %s", ar);
final SnapshotController snapController = mController.mSnapshotController;
if (mTransientLaunches != null && !task.isVisibleRequested()
&& !task.isActivityTypeHome()) {
final long startTimeNs = mLogger.mSendTimeNs;
final long lastSnapshotTimeNs = snapController.mTaskSnapshotController
.getSnapshotCaptureTime(task.mTaskId);
// If transition is transient, then snapshots are taken at end of
// transition only if a snapshot was not already captured by request
// during the transition
if (lastSnapshotTimeNs < startTimeNs) {
snapController.mTaskSnapshotController.recordSnapshot(task);
} else {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Skipping post-transition snapshot for task %d",
task.mTaskId);
}
}
ar.commitVisibility(false /* visible */, false /* performLayout */,
true /* fromTransition */);
committedSomeInvisible = true;
} else {
enterAutoPip = true;
}
}
final ChangeInfo changeInfo = mChanges.get(ar);
// Due to transient-hide, there may be some activities here which weren't in the
// transition.
if (changeInfo != null && changeInfo.mVisible != visibleAtTransitionEnd) {
// Legacy dispatch relies on this (for now).
ar.mEnteringAnimation = visibleAtTransitionEnd;
} else if (mTransientLaunches != null && mTransientLaunches.containsKey(ar)
&& ar.isVisible()) {
// Transient launch was committed, so report enteringAnimation
ar.mEnteringAnimation = true;
hasVisibleTransientLaunch = true;
// Since transient launches don't automatically take focus, make sure we
// synchronize focus since we committed to the launch.
if (!task.isFocused() && ar.isTopRunningActivity()) {
mController.mAtm.setLastResumedActivityUncheckLocked(ar,
"transitionFinished");
}
}
continue;
}
if (participant.asDisplayContent() != null) {
hasParticipatedDisplay = true;
continue;
}
final Task tr = participant.asTask();
if (tr != null && tr.isVisibleRequested() && tr.inPinnedWindowingMode()) {
final ActivityRecord top = tr.getTopNonFinishingActivity();
if (top != null && !top.inPinnedWindowingMode()) {
mController.mStateValidators.add(() -> {
if (!tr.isAttached() || !tr.isVisibleRequested()
|| !tr.inPinnedWindowingMode()) return;
final ActivityRecord currTop = tr.getTopNonFinishingActivity();
if (currTop.inPinnedWindowingMode()) return;
Slog.e(TAG, "Enter-PIP was started but not completed, this is a Shell/SysUI"
+ " bug. This state breaks gesture-nav, so attempting clean-up.");
// We don't know the destination bounds, so we can't actually finish the
// operation. So, to prevent the half-pipped task from covering everything,
// abort the action (which moves the task to back).
tr.abortPipEnter(currTop);
});
}
}
}
// Commit wallpaper visibility after activity, because usually the wallpaper target token is
// an activity, and wallpaper's visibility depends on activity's visibility.
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final WallpaperWindowToken wt = mParticipants.valueAt(i).asWallpaperToken();
if (wt == null) continue;
final WindowState target = wt.mDisplayContent.mWallpaperController.getWallpaperTarget();
final boolean isTargetInvisible = target == null || !target.mToken.isVisible();
final boolean isWallpaperVisibleAtEnd =
wt.isVisibleRequested() || mVisibleAtTransitionEndTokens.contains(wt);
if (isTargetInvisible || !isWallpaperVisibleAtEnd) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Commit wallpaper becoming invisible: %s", wt);
wt.commitVisibility(false /* visible */);
}
if (isTargetInvisible) {
// Our original target went invisible, so we should look for a new target.
wt.mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
}
}
if (committedSomeInvisible) {
mController.onCommittedInvisibles();
}
if (hasVisibleTransientLaunch) {
// Notify the change about the transient-below task if entering auto-pip.
if (enterAutoPip) {
mController.mAtm.getTaskChangeNotificationController().notifyTaskStackChanged();
}
// Prevent spurious background app switches.
mController.mAtm.stopAppSwitches();
// The end of transient launch may not reorder task, so make sure to compute the latest
// task rank according to the current visibility.
mController.mAtm.mRootWindowContainer.rankTaskLayers();
}
// dispatch legacy callback in a different loop. This is because multiple legacy handlers
// (fixed-rotation/displaycontent) make global changes, so we want to ensure that we've
// processed all the participants first (in particular, we want to trigger pip-enter first)
for (int i = 0; i < mParticipants.size(); ++i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
// If the activity was just inserted to an invisible task, it will keep INITIALIZING
// state. Then no need to notify the callback to avoid clearing some states
// unexpectedly, e.g. launch-task-behind.
if (ar != null && (ar.isVisibleRequested()
|| !ar.isState(ActivityRecord.State.INITIALIZING))) {
mController.dispatchLegacyAppTransitionFinished(ar);
}
}
// Update the input-sink (touch-blocking) state now that the animation is finished.
SurfaceControl.Transaction inputSinkTransaction = null;
for (int i = 0; i < mParticipants.size(); ++i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
if (ar == null || !ar.isVisible() || ar.getParent() == null) continue;
if (inputSinkTransaction == null) {
inputSinkTransaction = ar.mWmService.mTransactionFactory.get();
}
ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(inputSinkTransaction);
}
if (inputSinkTransaction != null) inputSinkTransaction.apply();
// Always schedule stop processing when transition finishes because activities don't
// stop while they are in a transition thus their stop could still be pending.
mController.mAtm.mTaskSupervisor
.scheduleProcessStoppingAndFinishingActivitiesIfNeeded();
sendRemoteCallback(mClientAnimationFinishCallback);
legacyRestoreNavigationBarFromApp();
if (mRecentsDisplayId != INVALID_DISPLAY) {
// Clean up input monitors (for recents)
final DisplayContent dc =
mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
dc.getInputMonitor().setActiveRecents(null /* activity */, null /* layer */);
dc.getInputMonitor().updateInputWindowsLw(false /* force */);
}
if (mTransientLaunches != null) {
for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
// Reset the ability of controlling SystemUi which might be changed by
// setTransientLaunch or setRecentsAppBehindSystemBars.
final Task task = mTransientLaunches.keyAt(i).getTask();
if (task != null) {
task.setCanAffectSystemUiFlags(true);
}
}
}
for (int i = 0; i < mTargetDisplays.size(); ++i) {
final DisplayContent dc = mTargetDisplays.get(i);
final AsyncRotationController asyncRotationController = dc.getAsyncRotationController();
if (asyncRotationController != null && containsChangeFor(dc, mTargets)) {
asyncRotationController.onTransitionFinished();
}
dc.onTransitionFinished();
if (hasParticipatedDisplay && dc.mDisplayRotationCompatPolicy != null) {
final ChangeInfo changeInfo = mChanges.get(dc);
if (changeInfo != null
&& changeInfo.mRotation != dc.getWindowConfiguration().getRotation()) {
dc.mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
}
}
if (mTransientLaunches != null) {
InsetsControlTarget prevImeTarget = dc.getImeTarget(
DisplayContent.IME_TARGET_CONTROL);
InsetsControlTarget newImeTarget = null;
TaskDisplayArea transientTDA = null;
// Transient-launch activities cannot be IME target (WindowState#canBeImeTarget),
// so re-compute in case the IME target is changed after transition.
for (int t = 0; t < mTransientLaunches.size(); ++t) {
if (mTransientLaunches.keyAt(t).getDisplayContent() == dc) {
newImeTarget = dc.computeImeTarget(true /* updateImeTarget */);
transientTDA = mTransientLaunches.keyAt(i).getTaskDisplayArea();
break;
}
}
if (mRecentsDisplayId != INVALID_DISPLAY && prevImeTarget == newImeTarget) {
// Restore IME icon only when moving the original app task to front from
// recents, in case IME icon may missing if the moving task has already been
// the current focused task.
InputMethodManagerInternal.get().updateImeWindowStatus(
false /* disableImeIcon */, dc.getDisplayId());
}
// An uncommitted transient launch can leave incomplete lifecycles if visibilities
// didn't change (eg. re-ordering with translucent tasks will leave launcher
// in RESUMED state), so force an update here.
if (!hasVisibleTransientLaunch && transientTDA != null) {
transientTDA.pauseBackTasks(null /* resuming */);
}
}
dc.removeImeSurfaceImmediately();
dc.handleCompleteDeferredRemoval();
}
validateKeyguardOcclusion();
mState = STATE_FINISHED;
// Rotation change may be deferred while there is a display change transition, so check
// again in case there is a new pending change.
if (hasParticipatedDisplay && !mController.useShellTransitionsRotation()) {
mController.mAtm.mWindowManager.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
}
cleanUpInternal();
// Handle back animation if it's already started.
mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this);
mController.mFinishingTransition = null;
mController.mSnapshotController.onTransitionFinish(mType, mTargets);
// Resume snapshot persist thread after snapshot controller analysis this transition.
mController.updateAnimatingState();
}
@Nullable
private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) {
if (mTransientLaunches == null) return null;
for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
final ActivityRecord candidateActivity = mTransientLaunches.keyAt(i);
if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
continue;
}
if (!candidateActivity.isVisibleRequested()) {
continue;
}
return candidateActivity;
}
return null;
}
void abort() {
// This calls back into itself via controller.abort, so just early return here.
if (mState == STATE_ABORT) return;
if (mState == STATE_PENDING) {
// hasn't started collecting, so can jump directly to aborted state.
mState = STATE_ABORT;
return;
}
if (mState != STATE_COLLECTING && mState != STATE_STARTED) {
throw new IllegalStateException("Too late to abort. state=" + mState);
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Aborting Transition: %d", mSyncId);
mState = STATE_ABORT;
mLogger.mAbortTimeNs = SystemClock.elapsedRealtimeNanos();
mController.mTransitionTracer.logAbortedTransition(this);
// Syncengine abort will call through to onTransactionReady()
mSyncEngine.abort(mSyncId);
mController.dispatchLegacyAppTransitionCancelled();
}
/** Immediately moves this to playing even if it isn't started yet. */
void playNow() {
if (!(mState == STATE_COLLECTING || mState == STATE_STARTED)) {
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d",
mSyncId);
mForcePlaying = true;
// backwards since conditions are removed.
for (int i = mReadyTracker.mConditions.size() - 1; i >= 0; --i) {
mReadyTracker.mConditions.get(i).meetAlternate("play-now");
}
final ReadyCondition forcePlay = new ReadyCondition("force-play-now");
mReadyTracker.add(forcePlay);
forcePlay.meet();
setAllReady();
if (mState == STATE_COLLECTING) {
start();
}
// Don't wait for actual surface-placement. We don't want anything else collected in this
// transition.
mSyncEngine.onSurfacePlacement();
}
boolean isForcePlaying() {
return mForcePlaying;
}
/** Adjusts the priority of the process which will run the transition animation. */
void setRemoteAnimationApp(IApplicationThread app) {
final WindowProcessController wpc = mController.mAtm.getProcessController(app);
if (wpc != null) {
// This is an early prediction. If the process doesn't ack the animation in 200 ms,
// the priority will be restored.
mController.mRemotePlayer.update(wpc, true /* running */, true /* predict */);
}
}
void setNoAnimation(WindowContainer wc) {
final ChangeInfo change = mChanges.get(wc);
if (change == null) {
throw new IllegalStateException("Can't set no-animation property of non-participant");
}
change.mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static boolean containsChangeFor(WindowContainer wc, ArrayList<ChangeInfo> list) {
for (int i = list.size() - 1; i >= 0; --i) {
if (list.get(i).mContainer == wc) return true;
}
return false;
}
@Override
public void onTransactionReady(int syncId, SurfaceControl.Transaction transaction) {
if (syncId != mSyncId) {
Slog.e(TAG, "Unexpected Sync ID " + syncId + ". Expected " + mSyncId);
return;
}
if (mController.useFullReadyTracking()) {
if (mReadyTracker.mMet.isEmpty()) {
Slog.e(TAG, "#" + mSyncId + ": No conditions provided");
} else {
for (int i = 0; i < mReadyTracker.mConditions.size(); ++i) {
Slog.e(TAG, "#" + mSyncId + ": unmet condition at ready: "
+ mReadyTracker.mConditions.get(i));
}
}
for (int i = 0; i < mReadyTracker.mMet.size(); ++i) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "#%d: Met condition: %s",
mSyncId, mReadyTracker.mMet.get(i));
}
}
// Commit the visibility of visible activities before calculateTransitionInfo(), so the
// TaskInfo can be visible. Also it needs to be done before moveToPlaying(), otherwise
// ActivityRecord#canShowWindows() may reject to show its window. The visibility also
// needs to be updated for STATE_ABORT.
commitVisibleActivities(transaction);
commitVisibleWallpapers();
// Fall-back to the default display if there isn't one participating.
final DisplayContent primaryDisplay = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0)
: mController.mAtm.mRootWindowContainer.getDefaultDisplay();
if (mState == STATE_ABORT) {
mController.onAbort(this);
primaryDisplay.getPendingTransaction().merge(transaction);
mSyncId = -1;
mOverrideOptions = null;
cleanUpInternal();
return;
}
if (mState != STATE_STARTED) {
Slog.e(TAG, "Playing a Transition which hasn't started! #" + mSyncId + " This will "
+ "likely cause an exception in Shell");
}
mState = STATE_PLAYING;
mStartTransaction = transaction;
mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
// Flags must be assigned before calculateTransitionInfo. Otherwise it won't take effect.
if (primaryDisplay.isKeyguardLocked()) {
mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
}
// This is the only (or last) transition that is collecting, so we need to report any
// leftover order changes.
collectOrderChanges(mController.mWaitingTransitions.isEmpty());
if (mPriorVisibilityMightBeDirty) {
updatePriorVisibility();
}
// Resolve the animating targets from the participants.
mTargets = calculateTargets(mParticipants, mChanges);
// Check whether the participants were animated from back navigation.
mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets,
transaction);
final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction);
info.setDebugId(mSyncId);
mController.assignTrack(this, info);
mController.moveToPlaying(this);
// Repopulate the displays based on the resolved targets.
mTargetDisplays.clear();
for (int i = 0; i < info.getRootCount(); ++i) {
final DisplayContent dc = mController.mAtm.mRootWindowContainer.getDisplayContent(
info.getRoot(i).getDisplayId());
mTargetDisplays.add(dc);
}
for (int i = 0; i < mTargets.size(); ++i) {
final DisplayArea da = mTargets.get(i).mContainer.asDisplayArea();
if (da == null) continue;
if (da.isVisibleRequested()) {
mController.mValidateDisplayVis.remove(da);
} else {
// In case something accidentally hides a displayarea and nothing shows it again.
mController.mValidateDisplayVis.add(da);
}
}
if (mOverrideOptions != null) {
info.setAnimationOptions(mOverrideOptions);
if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
for (int i = 0; i < mTargets.size(); ++i) {
final TransitionInfo.Change c = info.getChanges().get(i);
final ActivityRecord ar = mTargets.get(i).mContainer.asActivityRecord();
if (ar == null || c.getMode() != TRANSIT_OPEN) continue;
int flags = c.getFlags();
flags |= ar.mUserId == ar.mWmService.mCurrentUserId
? TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL
: TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
c.setFlags(flags);
break;
}
}
}
// TODO(b/188669821): Move to animation impl in shell.
for (int i = 0; i < mTargetDisplays.size(); ++i) {
handleLegacyRecentsStartBehavior(mTargetDisplays.get(i), info);
if (mRecentsDisplayId != INVALID_DISPLAY) break;
}
// The callback is only populated for custom activity-level client animations
sendRemoteCallback(mClientAnimationStartCallback);
// Manually show any activities that are visibleRequested. This is needed to properly
// support simultaneous animation queueing/merging. Specifically, if transition A makes
// an activity invisible, it's finishTransaction (which is applied *after* the animation)
// will hide the activity surface. If transition B then makes the activity visible again,
// the normal surfaceplacement logic won't add a show to this start transaction because
// the activity visibility hasn't been committed yet. To deal with this, we have to manually
// show here in the same way that we manually hide in finishTransaction.
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
if (ar == null || !ar.isVisibleRequested()) continue;
transaction.show(ar.getSurfaceControl());
// Also manually show any non-reported parents. This is necessary in a few cases
// where a task is NOT organized but had its visibility changed within its direct
// parent. An example of this is if an alternate home leaf-task HB is started atop the
// normal home leaf-task HA: these are both in the Home root-task HR, so there will be a
// transition containing HA and HB where HA surface is hidden. If a standard task SA is
// launched on top, then HB finishes, no transition will happen since neither home is
// visible. When SA finishes, the transition contains HR rather than HA. Since home
// leaf-tasks are NOT organized, HA won't be in the transition and thus its surface
// wouldn't be shown. Just show is safe here since all other properties will have
// already been reset by the original hiding-transition's finishTransaction (we can't
// show in the finishTransaction because by then the activity doesn't hide until
// surface placement).
for (WindowContainer p = ar.getParent(); p != null && !containsChangeFor(p, mTargets);
p = p.getParent()) {
if (p.getSurfaceControl() != null) {
transaction.show(p.getSurfaceControl());
}
}
}
// Record windowtokens (activity/wallpaper) that are expected to be visible after the
// transition animation. This will be used in finishTransition to prevent prematurely
// committing visibility. Skip transient launches since those are only temporarily visible.
if (mTransientLaunches == null) {
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final WindowContainer wc = mParticipants.valueAt(i);
if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue;
mVisibleAtTransitionEndTokens.add(wc.asWindowToken());
}
}
// This is non-null only if display has changes. It handles the visible windows that don't
// need to be participated in the transition.
for (int i = 0; i < mTargetDisplays.size(); ++i) {
final DisplayContent dc = mTargetDisplays.get(i);
final AsyncRotationController controller = dc.getAsyncRotationController();
if (controller != null && containsChangeFor(dc, mTargets)) {
controller.setupStartTransaction(transaction);
}
}
buildFinishTransaction(mFinishTransaction, info);
mCleanupTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
buildCleanupTransaction(mCleanupTransaction, info);
if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Calling onTransitionReady: %s", info);
mLogger.mSendTimeNs = SystemClock.elapsedRealtimeNanos();
mLogger.mInfo = info;
mController.getTransitionPlayer().onTransitionReady(
mToken, info, transaction, mFinishTransaction);
if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
asyncTraceBegin(TRACE_NAME_PLAY_TRANSITION, System.identityHashCode(this));
}
} catch (RemoteException e) {
// If there's an exception when trying to send the mergedTransaction to the
// client, we should finish and apply it here so the transactions aren't lost.
postCleanupOnFailure();
}
for (int i = 0; i < mTargetDisplays.size(); ++i) {
final DisplayContent dc = mTargetDisplays.get(i);
final AccessibilityController accessibilityController =
dc.mWmService.mAccessibilityController;
if (accessibilityController.hasCallbacks()) {
accessibilityController.onWMTransition(dc.getDisplayId(), mType);
}
}
} else {
// No player registered or it's not enabled, so just finish/apply immediately
if (!mIsPlayerEnabled) {
mLogger.mSendTimeNs = SystemClock.uptimeNanos();
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Apply and finish immediately"
+ " because player is disabled for transition #%d .", mSyncId);
}
postCleanupOnFailure();
}
mOverrideOptions = null;
reportStartReasonsToLogger();
// Take snapshots for closing tasks/activities before the animation finished but after
// dispatching onTransitionReady, so IME (if there is) can be captured together and the
// time spent on snapshot won't delay the start of animation. Note that if this transition
// is transient (mTransientLaunches != null), the snapshot will be captured at the end of
// the transition, because IME won't move be moved during the transition and the tasks are
// still live.
if (mTransientLaunches == null) {
mController.mSnapshotController.onTransactionReady(mType, mTargets);
}
// Since we created root-leash but no longer reference it from core, release it now
info.releaseAnimSurfaces();
if (mLogger.mInfo != null) {
mLogger.logOnSendAsync(mController.mLoggerHandler);
mController.mTransitionTracer.logSentTransition(this, mTargets);
}
}
@Override
public void onTransactionCommitTimeout() {
if (mCleanupTransaction == null) return;
for (int i = mTargetDisplays.size() - 1; i >= 0; --i) {
final DisplayContent dc = mTargetDisplays.get(i);
final AsyncRotationController asyncRotationController = dc.getAsyncRotationController();
if (asyncRotationController != null && containsChangeFor(dc, mTargets)) {
asyncRotationController.onTransactionCommitTimeout(mCleanupTransaction);
}
}
}
/**
* Checks if the transition contains order changes.
*
* This is a shallow check that doesn't account for collection in parallel, unlike
* {@code collectOrderChanges}
*/
boolean hasOrderChanges() {
ArrayList<Task> onTopTasks = new ArrayList<>();
// Iterate over target displays to get up to date on top tasks.
// Cannot use `mOnTopTasksAtReady` as it's not populated before the `applyReady` is called.
for (DisplayContent dc : mTargetDisplays) {
addOnTopTasks(dc, onTopTasks);
}
for (Task task : onTopTasks) {
if (!mOnTopTasksStart.contains(task)) {
return true;
}
}
return false;
}
/**
* Collect tasks which moved-to-top as part of this transition. This also updates the
* controller's latest-reported when relevant.
*
* This is a non-trivial operation because transition can collect in parallel; however, it can
* be made tenable by acknowledging that the "setup" part of collection (phase 1) is still
* globally serial; so, we can build some reasonable rules around it.
*
* First, we record the "start" on-top state (to compare against). Then, when this becomes
* ready (via allReady, NOT onTransactionReady), we also record the "onReady" on-top state
* -- the idea here is that upon "allReady", all the actual WM changes should be done and we
* are now just waiting for window content to become ready (finish drawing).
*
* Then, in this function (during onTransactionReady), we compare the two orders and include
* any changes to the order in the reported transition-info. Unfortunately, because of parallel
* collection, the order can change in unexpected ways by now. To resolve this, we ALSO keep a
* global "latest reported order" in TransitionController and use that to make decisions.
*/
@VisibleForTesting
void collectOrderChanges(boolean reportCurrent) {
if (mOnTopTasksStart.isEmpty()) return;
boolean includesOrderChange = false;
for (int i = 0; i < mOnTopTasksAtReady.size(); ++i) {
final Task task = mOnTopTasksAtReady.get(i);
if (mOnTopTasksStart.contains(task)) continue;
includesOrderChange = true;
break;
}
if (!includesOrderChange && !reportCurrent) {
// This transition doesn't include an order change, so if it isn't required to report
// the current focus (eg. it's the last of a cluster of transitions), then don't
// report.
return;
}
// The transition included an order change, but it may not be up-to-date, so grab the
// latest state and compare with the last reported state (or our start state if no
// reported state exists).
ArrayList<Task> onTopTasksEnd = new ArrayList<>();
for (int d = 0; d < mTargetDisplays.size(); ++d) {
addOnTopTasks(mTargetDisplays.get(d), onTopTasksEnd);
final int displayId = mTargetDisplays.get(d).mDisplayId;
ArrayList<Task> reportedOnTop = mController.mLatestOnTopTasksReported.get(displayId);
for (int i = onTopTasksEnd.size() - 1; i >= 0; --i) {
final Task task = onTopTasksEnd.get(i);
if (task.getDisplayId() != displayId) continue;
// If it didn't change since last report, don't report
if (reportedOnTop == null) {
if (mOnTopTasksStart.contains(task)) continue;
} else if (reportedOnTop.contains(task)) {
continue;
}
// Need to report it.
mParticipants.add(task);
int changeIdx = mChanges.indexOfKey(task);
if (changeIdx < 0) {
mChanges.put(task, new ChangeInfo(task));
changeIdx = mChanges.indexOfKey(task);
}
mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP;
}
// Swap in the latest on-top tasks.
mController.mLatestOnTopTasksReported.put(displayId, onTopTasksEnd);
onTopTasksEnd = reportedOnTop != null ? reportedOnTop : new ArrayList<>();
onTopTasksEnd.clear();
}
}
private void postCleanupOnFailure() {
mController.mAtm.mH.post(() -> {
synchronized (mController.mAtm.mGlobalLock) {
cleanUpOnFailure();
}
});
}
/**
* If the remote failed for any reason, use this to do any appropriate clean-up. Do not call
* this directly, it's designed to by called by {@link TransitionController} only.
*/
void cleanUpOnFailure() {
// No need to clean-up if this isn't playing yet.
if (mState < STATE_PLAYING) return;
if (mStartTransaction != null) {
mStartTransaction.apply();
}
if (mFinishTransaction != null) {
mFinishTransaction.apply();
}
mController.finishTransition(this);
}
private void cleanUpInternal() {
// Clean-up any native references.
for (int i = 0; i < mChanges.size(); ++i) {
final ChangeInfo ci = mChanges.valueAt(i);
if (ci.mSnapshot != null) {
ci.mSnapshot.release();
}
}
if (mCleanupTransaction != null) {
mCleanupTransaction.apply();
mCleanupTransaction = null;
}
}
/** The transition is ready to play. Make the start transaction show the surfaces. */
private void commitVisibleActivities(SurfaceControl.Transaction transaction) {
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
if (ar == null || ar.getTask() == null) {
continue;
}
if (ar.isVisibleRequested()) {
ar.commitVisibility(true /* visible */, false /* performLayout */,
true /* fromTransition */);
ar.commitFinishDrawing(transaction);
}
ar.getTask().setDeferTaskAppear(false);
}
}
/**
* Reset waitingToshow for all wallpapers, and commit the visibility of the visible ones
*/
private void commitVisibleWallpapers() {
boolean showWallpaper = shouldWallpaperBeVisible();
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken();
if (wallpaper != null) {
wallpaper.waitingToShow = false;
if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) {
wallpaper.commitVisibility(showWallpaper);
}
}
}
}
private boolean shouldWallpaperBeVisible() {
for (int i = mParticipants.size() - 1; i >= 0; --i) {
WindowContainer participant = mParticipants.valueAt(i);
if (participant.showWallpaper()) return true;
}
return false;
}
// TODO(b/188595497): Remove after migrating to shell.
/** @see RecentsAnimationController#attachNavigationBarToApp */
private void handleLegacyRecentsStartBehavior(DisplayContent dc, TransitionInfo info) {
if ((mFlags & TRANSIT_FLAG_IS_RECENTS) == 0) {
return;
}
// Recents has an input-consumer to grab input from the "live tile" app. Set that up here
final InputConsumerImpl recentsAnimationInputConsumer =
dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
ActivityRecord recentsActivity = null;
if (recentsAnimationInputConsumer != null) {
// find the top-most going-away activity and the recents activity. The top-most
// is used as layer reference while the recents is used for registering the consumer
// override.
ActivityRecord topActivity = null;
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.getTaskInfo() == null) continue;
final Task task = Task.fromWindowContainerToken(
info.getChanges().get(i).getTaskInfo().token);
if (task == null) continue;
final int activityType = change.getTaskInfo().topActivityType;
final boolean isRecents = activityType == ACTIVITY_TYPE_HOME
|| activityType == ACTIVITY_TYPE_RECENTS;
if (isRecents && recentsActivity == null) {
recentsActivity = task.getTopVisibleActivity();
} else if (!isRecents && topActivity == null) {
topActivity = task.getTopNonFinishingActivity();
}
}
if (recentsActivity != null && topActivity != null) {
recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(
topActivity.getBounds());
dc.getInputMonitor().setActiveRecents(recentsActivity, topActivity);
}
}
if (recentsActivity == null) {
// No recents activity on `dc`, its probably on a different display.
return;
}
mRecentsDisplayId = dc.mDisplayId;
// The rest of this function handles nav-bar reparenting
if (!dc.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()
// Skip the case where the nav bar is controlled by fade rotation.
|| dc.getAsyncRotationController() != null) {
return;
}
WindowContainer topWC = null;
// Find the top-most non-home, closing app.
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change c = info.getChanges().get(i);
if (c.getTaskInfo() == null || c.getTaskInfo().displayId != mRecentsDisplayId
|| c.getTaskInfo().getActivityType() != ACTIVITY_TYPE_STANDARD
|| !(c.getMode() == TRANSIT_CLOSE || c.getMode() == TRANSIT_TO_BACK)) {
continue;
}
topWC = WindowContainer.fromBinder(c.getContainer().asBinder());
break;
}
if (topWC == null || topWC.inMultiWindowMode()) {
return;
}
final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar();
if (navWindow == null || navWindow.mToken == null) {
return;
}
mController.mNavigationBarAttachedToApp = true;
navWindow.mToken.cancelAnimation();
final SurfaceControl.Transaction t = navWindow.mToken.getPendingTransaction();
final SurfaceControl navSurfaceControl = navWindow.mToken.getSurfaceControl();
t.reparent(navSurfaceControl, topWC.getSurfaceControl());
t.show(navSurfaceControl);
final WindowContainer imeContainer = dc.getImeContainer();
if (imeContainer.isVisible()) {
t.setRelativeLayer(navSurfaceControl, imeContainer.getSurfaceControl(), 1);
} else {
// Place the nav bar on top of anything else in the top activity.
t.setLayer(navSurfaceControl, Integer.MAX_VALUE);
}
final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal();
if (bar != null) {
bar.setNavigationBarLumaSamplingEnabled(mRecentsDisplayId, false);
}
}
/** @see RecentsAnimationController#restoreNavigationBarFromApp */
void legacyRestoreNavigationBarFromApp() {
if (!mController.mNavigationBarAttachedToApp) {
return;
}
mController.mNavigationBarAttachedToApp = false;
if (mRecentsDisplayId == INVALID_DISPLAY) {
Slog.e(TAG, "Reparented navigation bar without a valid display");
mRecentsDisplayId = DEFAULT_DISPLAY;
}
final DisplayContent dc =
mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal();
if (bar != null) {
bar.setNavigationBarLumaSamplingEnabled(mRecentsDisplayId, true);
}
final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar();
if (navWindow == null) return;
navWindow.setSurfaceTranslationY(0);
final WindowToken navToken = navWindow.mToken;
if (navToken == null) return;
final SurfaceControl.Transaction t = dc.getPendingTransaction();
final WindowContainer parent = navToken.getParent();
t.setLayer(navToken.getSurfaceControl(), navToken.getLastLayer());
boolean animate = false;
// Search for the home task. If it is supposed to be visible, then the navbar is not at
// the bottom of the screen, so we need to animate it.
for (int i = 0; i < mTargets.size(); ++i) {
final Task task = mTargets.get(i).mContainer.asTask();
if (task == null || !task.isActivityTypeHomeOrRecents()) continue;
animate = task.isVisibleRequested();
break;
}
if (animate) {
final NavBarFadeAnimationController controller =
new NavBarFadeAnimationController(dc);
controller.fadeWindowToken(true);
} else {
// Reparent the SurfaceControl of nav bar token back.
t.reparent(navToken.getSurfaceControl(), parent.getSurfaceControl());
}
// To apply transactions.
dc.mWmService.scheduleAnimationLocked();
}
private void reportStartReasonsToLogger() {
// Record transition start in metrics logger. We just assume everything is "DRAWN"
// at this point since splash-screen is a presentation (shell) detail.
ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>();
for (int i = mParticipants.size() - 1; i >= 0; --i) {
ActivityRecord r = mParticipants.valueAt(i).asActivityRecord();
if (r == null || !r.isVisibleRequested()) continue;
int transitionReason = APP_TRANSITION_WINDOWS_DRAWN;
// At this point, r is "ready", but if it's not "ALL ready" then it is probably only
// ready due to starting-window.
if (r.mStartingData instanceof SplashScreenStartingData && !r.mLastAllReadyAtSync) {
transitionReason = APP_TRANSITION_SPLASH_SCREEN;
} else if (r.isActivityTypeHomeOrRecents() && isTransientLaunch(r)) {
transitionReason = APP_TRANSITION_RECENTS_ANIM;
}
reasons.put(r, transitionReason);
}
mController.mAtm.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting(
reasons);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append("TransitionRecord{");
sb.append(Integer.toHexString(System.identityHashCode(this)));
sb.append(" id=" + mSyncId);
sb.append(" type=" + transitTypeToString(mType));
sb.append(" flags=0x" + Integer.toHexString(mFlags));
sb.append('}');
return sb.toString();
}
/** Returns the parent that the remote animator can animate or control. */
private static WindowContainer<?> getAnimatableParent(WindowContainer<?> wc) {
WindowContainer<?> parent = wc.getParent();
while (parent != null
&& (!parent.canCreateRemoteAnimationTarget() && !parent.isOrganized())) {
parent = parent.getParent();
}
return parent;
}
private static boolean reportIfNotTop(WindowContainer wc) {
// Organized tasks need to be reported anyways because Core won't show() their surfaces
// and we can't rely on onTaskAppeared because it isn't in sync.
// TODO(shell-transitions): switch onTaskAppeared usage over to transitions OPEN.
return wc.isOrganized();
}
private static boolean isWallpaper(WindowContainer wc) {
return wc.asWallpaperToken() != null;
}
private static boolean isInputMethod(WindowContainer wc) {
return wc.getWindowType() == TYPE_INPUT_METHOD;
}
private static boolean occludesKeyguard(WindowContainer wc) {
final ActivityRecord ar = wc.asActivityRecord();
if (ar != null) {
return ar.canShowWhenLocked();
}
final Task t = wc.asTask();
if (t != null) {
// Get the top activity which was visible (since this is going away, it will remain
// client visible until the transition is finished).
// skip hidden (or about to hide) apps
final ActivityRecord top = t.getActivity(WindowToken::isClientVisible);
return top != null && top.canShowWhenLocked();
}
return false;
}
private static boolean isTranslucent(@NonNull WindowContainer wc) {
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment == null) {
return !wc.fillsParent();
}
// Check containers differently as they are affected by child visibility.
if (taskFragment.isTranslucentForTransition()) {
// TaskFragment doesn't contain occluded ActivityRecord.
return true;
}
final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
if (adjacentTaskFragment != null) {
// When the TaskFragment has an adjacent TaskFragment, sibling behind them should be
// hidden unless any of them are translucent.
return adjacentTaskFragment.isTranslucentForTransition();
} else {
// Non-filling without adjacent is considered as translucent.
return !wc.fillsParent();
}
}
private void updatePriorVisibility() {
for (int i = 0; i < mChanges.size(); ++i) {
final ChangeInfo chg = mChanges.valueAt(i);
// For task/activity, recalculate the current "real" visibility.
if (chg.mContainer.asActivityRecord() == null && chg.mContainer.asTask() == null) {
continue;
}
// This ONLY works in the visible -> invisible case (and is only needed for this case)
// because commitVisible(false) is deferred until finish.
if (!chg.mVisible) continue;
chg.mVisible = chg.mContainer.isVisible();
}
}
/**
* Under some conditions (eg. all visible targets within a parent container are transitioning
* the same way) the transition can be "promoted" to the parent container. This means an
* animation can play just on the parent rather than all the individual children.
*
* @return {@code true} if transition in target can be promoted to its parent.
*/
private static boolean canPromote(ChangeInfo targetChange, Targets targets,
ArrayMap<WindowContainer, ChangeInfo> changes) {
final WindowContainer<?> target = targetChange.mContainer;
final WindowContainer<?> parent = target.getParent();
final ChangeInfo parentChange = changes.get(parent);
if (!parent.canCreateRemoteAnimationTarget()
|| parentChange == null || !parentChange.hasChanged()) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " SKIP: %s",
"parent can't be target " + parent);
return false;
}
if (isWallpaper(target)) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " SKIP: is wallpaper");
return false;
}
if (targetChange.mStartParent != null && target.getParent() != targetChange.mStartParent) {
// When a window is reparented, the state change won't fit into any of the parents.
// Don't promote such change so that we can animate the reparent if needed.
return false;
}
final @TransitionInfo.TransitionMode int mode = targetChange.getTransitMode(target);
for (int i = parent.getChildCount() - 1; i >= 0; --i) {
final WindowContainer<?> sibling = parent.getChildAt(i);
if (target == sibling) continue;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " check sibling %s",
sibling);
final ChangeInfo siblingChange = changes.get(sibling);
if (siblingChange == null || !targets.wasParticipated(siblingChange)) {
if (sibling.isVisibleRequested()) {
// Sibling is visible but not animating, so no promote.
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" SKIP: sibling is visible but not part of transition");
return false;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" unrelated invisible sibling %s", sibling);
continue;
}
final int siblingMode = siblingChange.getTransitMode(sibling);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" sibling is a participant with mode %s",
TransitionInfo.modeToString(siblingMode));
if (reduceMode(mode) != reduceMode(siblingMode)) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" SKIP: common mode mismatch. was %s",
TransitionInfo.modeToString(mode));
return false;
}
}
return true;
}
/** "reduces" a mode into a smaller set of modes that uniquely represents visibility change. */
@TransitionInfo.TransitionMode
private static int reduceMode(@TransitionInfo.TransitionMode int mode) {
switch (mode) {
case TRANSIT_TO_BACK: return TRANSIT_CLOSE;
case TRANSIT_TO_FRONT: return TRANSIT_OPEN;
default: return mode;
}
}
/**
* Go through topTargets and try to promote (see {@link #canPromote}) one of them.
*
* @param targets all targets that will be sent to the player.
*/
private static void tryPromote(Targets targets, ArrayMap<WindowContainer, ChangeInfo> changes) {
WindowContainer<?> lastNonPromotableParent = null;
// Go through from the deepest target.
for (int i = targets.mArray.size() - 1; i >= 0; --i) {
final ChangeInfo targetChange = targets.mArray.valueAt(i);
final WindowContainer<?> target = targetChange.mContainer;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " checking %s", target);
final WindowContainer<?> parent = target.getParent();
if (parent == lastNonPromotableParent) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" SKIP: its sibling was rejected");
continue;
}
if (!canPromote(targetChange, targets, changes)) {
lastNonPromotableParent = parent;
continue;
}
if (reportIfNotTop(target)) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" keep as target %s", target);
} else {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" remove from targets %s", target);
targets.remove(i);
}
final ChangeInfo parentChange = changes.get(parent);
if (targets.mArray.indexOfValue(parentChange) < 0) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" CAN PROMOTE: promoting to parent %s", parent);
// The parent has lower depth, so it will be checked in the later iteration.
i++;
targets.add(parentChange);
}
if ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_NO_ANIMATION) != 0) {
parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
} else {
parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION;
}
}
}
/**
* Find WindowContainers to be animated from a set of opening and closing apps. We will promote
* animation targets to higher level in the window hierarchy if possible.
*/
@VisibleForTesting
@NonNull
static ArrayList<ChangeInfo> calculateTargets(ArraySet<WindowContainer> participants,
ArrayMap<WindowContainer, ChangeInfo> changes) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Start calculating TransitionInfo based on participants: %s", participants);
// Add all valid participants to the target container.
final Targets targets = new Targets();
for (int i = participants.size() - 1; i >= 0; --i) {
final WindowContainer<?> wc = participants.valueAt(i);
if (!wc.isAttached()) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Rejecting as detached: %s", wc);
continue;
}
// The level of transition target should be at least window token.
if (wc.asWindowState() != null) continue;
final ChangeInfo changeInfo = changes.get(wc);
// Reject no-ops
if (!changeInfo.hasChanged()) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Rejecting as no-op: %s", wc);
continue;
}
targets.add(changeInfo);
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Initial targets: %s",
targets.mArray);
// Combine the targets from bottom to top if possible.
tryPromote(targets, changes);
// Establish the relationship between the targets and their top changes.
populateParentChanges(targets, changes);
final ArrayList<ChangeInfo> targetList = targets.getListSortedByZ();
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Final targets: %s", targetList);
return targetList;
}
/** Populates parent to the change info and collects intermediate targets. */
private static void populateParentChanges(Targets targets,
ArrayMap<WindowContainer, ChangeInfo> changes) {
final ArrayList<ChangeInfo> intermediates = new ArrayList<>();
// Make a copy to iterate because the original array may be modified.
final ArrayList<ChangeInfo> targetList = new ArrayList<>(targets.mArray.size());
for (int i = targets.mArray.size() - 1; i >= 0; --i) {
targetList.add(targets.mArray.valueAt(i));
}
for (int i = targetList.size() - 1; i >= 0; --i) {
final ChangeInfo targetChange = targetList.get(i);
final WindowContainer wc = targetChange.mContainer;
// Wallpaper must belong to the top (regardless of how nested it is in DisplayAreas).
final boolean skipIntermediateReports = isWallpaper(wc);
intermediates.clear();
boolean foundParentInTargets = false;
// Collect the intermediate parents between target and top changed parent.
for (WindowContainer<?> p = getAnimatableParent(wc); p != null;
p = getAnimatableParent(p)) {
final ChangeInfo parentChange = changes.get(p);
if (parentChange == null || !parentChange.hasChanged()) break;
if (p.mRemoteToken == null) {
// Intermediate parents must be those that has window to be managed by Shell.
continue;
}
if (parentChange.mEndParent != null && !skipIntermediateReports) {
targetChange.mEndParent = p;
// The chain above the parent was processed.
break;
}
if (targetList.contains(parentChange)) {
if (skipIntermediateReports) {
targetChange.mEndParent = p;
} else {
intermediates.add(parentChange);
}
foundParentInTargets = true;
break;
} else if (reportIfNotTop(p) && !skipIntermediateReports) {
intermediates.add(parentChange);
}
}
if (!foundParentInTargets || intermediates.isEmpty()) continue;
// Add any always-report parents along the way.
targetChange.mEndParent = intermediates.get(0).mContainer;
for (int j = 0; j < intermediates.size() - 1; j++) {
final ChangeInfo intermediate = intermediates.get(j);
intermediate.mEndParent = intermediates.get(j + 1).mContainer;
targets.add(intermediate);
}
}
}
/**
* Gets the leash surface for a window container.
* @param t a transaction to create leashes on when necessary (fixed rotation at token-level).
* If t is null, then this will not create any leashes, just use one if it is there --
* this is relevant for building the finishTransaction since it needs to match the
* start state and not erroneously create a leash of its own.
*/
private static SurfaceControl getLeashSurface(WindowContainer wc,
@Nullable SurfaceControl.Transaction t) {
final DisplayContent asDC = wc.asDisplayContent();
if (asDC != null) {
// DisplayContent is the "root", so we use the windowing layer instead to avoid
// hardware-screen-level surfaces.
return asDC.getWindowingLayer();
}
if (!wc.mTransitionController.useShellTransitionsRotation()) {
final WindowToken asToken = wc.asWindowToken();
if (asToken != null) {
// WindowTokens can have a fixed-rotation applied to them. In the current
// implementation this fact is hidden from the player, so we must create a leash.
final SurfaceControl leash = t != null ? asToken.getOrCreateFixedRotationLeash(t)
: asToken.getFixedRotationLeash();
if (leash != null) return leash;
}
}
return wc.getSurfaceControl();
}
private static SurfaceControl getOrigParentSurface(WindowContainer wc) {
if (wc.asDisplayContent() != null) {
// DisplayContent is the "root", so we reinterpret it's wc as the window layer
// making the parent surface the displaycontent's surface.
return wc.getSurfaceControl();
} else if (wc.getParent().asDisplayContent() != null) {
// DisplayContent is kinda split into 2 pieces, the "real root" and the
// "windowing layer". So if the parent of the window is DC, then it really belongs on
// the windowing layer (unless it's an overlay display area, but those can't be in
// transitions anyways).
return wc.getParent().asDisplayContent().getWindowingLayer();
}
return wc.getParent().getSurfaceControl();
}
/**
* A ready group is defined by a root window-container where all transitioning windows under
* it are expected to animate together as a group. At the moment, this treats each display as
* a ready-group to match the existing legacy transition behavior.
*/
private static boolean isReadyGroup(WindowContainer wc) {
return wc instanceof DisplayContent;
}
private static int getDisplayId(@NonNull WindowContainer wc) {
return wc.getDisplayContent() != null
? wc.getDisplayContent().getDisplayId() : INVALID_DISPLAY;
}
@VisibleForTesting
static void calculateTransitionRoots(@NonNull TransitionInfo outInfo,
ArrayList<ChangeInfo> sortedTargets,
@NonNull SurfaceControl.Transaction startT) {
// There needs to be a root on each display.
for (int i = 0; i < sortedTargets.size(); ++i) {
final WindowContainer<?> wc = sortedTargets.get(i).mContainer;
// Don't include wallpapers since they are in a different DA.
if (isWallpaper(wc)) continue;
final DisplayContent dc = wc.getDisplayContent();
if (dc == null) continue;
final int endDisplayId = dc.getDisplayId();
// Check if Root was already created for this display with a higher-Z window
if (outInfo.findRootIndex(endDisplayId) >= 0) continue;
WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, wc);
// Make leash based on highest (z-order) direct child of ancestor with a participant.
// Check whether the ancestor is belonged to last parent, shouldn't happen.
final boolean hasReparent = !wc.isDescendantOf(ancestor);
WindowContainer leashReference = wc;
if (hasReparent) {
Slog.e(TAG, "Did not find common ancestor! Ancestor= " + ancestor
+ " target= " + wc);
} else {
while (leashReference.getParent() != ancestor) {
leashReference = leashReference.getParent();
}
}
final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName(
"Transition Root: " + leashReference.getName()).build();
rootLeash.setUnreleasedWarningCallSite("Transition.calculateTransitionRoots");
// Update layers to start transaction because we prevent assignment during collect, so
// the layer of transition root can be correct.
updateDisplayLayers(dc, startT);
startT.setLayer(rootLeash, leashReference.getLastLayer());
outInfo.addRootLeash(endDisplayId, rootLeash,
ancestor.getBounds().left, ancestor.getBounds().top);
}
}
/**
* Construct a TransitionInfo object from a set of targets and changes. Also populates the
* root surface.
* @param sortedTargets The targets sorted by z-order from top (index 0) to bottom.
* @param startT The start transaction - used to set-up new leashes.
*/
@VisibleForTesting
@NonNull
static TransitionInfo calculateTransitionInfo(@TransitionType int type, int flags,
ArrayList<ChangeInfo> sortedTargets,
@NonNull SurfaceControl.Transaction startT) {
final TransitionInfo out = new TransitionInfo(type, flags);
calculateTransitionRoots(out, sortedTargets, startT);
if (out.getRootCount() == 0) {
return out;
}
// Convert all the resolved ChangeInfos into TransactionInfo.Change objects in order.
final int count = sortedTargets.size();
for (int i = 0; i < count; ++i) {
final ChangeInfo info = sortedTargets.get(i);
final WindowContainer target = info.mContainer;
final TransitionInfo.Change change = new TransitionInfo.Change(
target.mRemoteToken != null ? target.mRemoteToken.toWindowContainerToken()
: null, getLeashSurface(target, startT));
// TODO(shell-transitions): Use leash for non-organized windows.
if (info.mEndParent != null) {
change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
}
if (info.mStartParent != null && info.mStartParent.mRemoteToken != null
&& target.getParent() != info.mStartParent) {
change.setLastParent(info.mStartParent.mRemoteToken.toWindowContainerToken());
}
change.setMode(info.getTransitMode(target));
info.mReadyMode = change.getMode();
change.setStartAbsBounds(info.mAbsoluteBounds);
change.setFlags(info.getChangeFlags(target));
info.mReadyFlags = change.getFlags();
change.setDisplayId(info.mDisplayId, getDisplayId(target));
final Task task = target.asTask();
final TaskFragment taskFragment = target.asTaskFragment();
final ActivityRecord activityRecord = target.asActivityRecord();
if (task != null) {
final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo();
task.fillTaskInfo(tinfo);
change.setTaskInfo(tinfo);
change.setRotationAnimation(getTaskRotationAnimation(task));
final ActivityRecord topRunningActivity = task.topRunningActivity();
if (topRunningActivity != null) {
if (topRunningActivity.info.supportsPictureInPicture()) {
change.setAllowEnterPip(
topRunningActivity.checkEnterPictureInPictureAppOpsState());
}
setEndFixedRotationIfNeeded(change, task, topRunningActivity);
}
} else if ((info.mFlags & ChangeInfo.FLAG_SEAMLESS_ROTATION) != 0) {
change.setRotationAnimation(ROTATION_ANIMATION_SEAMLESS);
}
final WindowContainer<?> parent = target.getParent();
final Rect bounds = target.getBounds();
final Rect parentBounds = parent.getBounds();
change.setEndRelOffset(bounds.left - parentBounds.left,
bounds.top - parentBounds.top);
int endRotation = target.getWindowConfiguration().getRotation();
if (activityRecord != null) {
// TODO(b/227427984): Shell needs to aware letterbox.
// Always use parent bounds of activity because letterbox area (e.g. fixed aspect
// ratio or size compat mode) should be included in the animation.
change.setEndAbsBounds(parentBounds);
if (activityRecord.getRelativeDisplayRotation() != 0
&& !activityRecord.mTransitionController.useShellTransitionsRotation()) {
// Use parent rotation because shell doesn't know the surface is rotated.
endRotation = parent.getWindowConfiguration().getRotation();
}
} else {
change.setEndAbsBounds(bounds);
}
if (activityRecord != null || (taskFragment != null && taskFragment.isEmbedded())) {
final int backgroundColor;
final TaskFragment organizedTf = activityRecord != null
? activityRecord.getOrganizedTaskFragment()
: taskFragment.getOrganizedTaskFragment();
if (organizedTf != null && organizedTf.getAnimationParams()
.getAnimationBackgroundColor() != DEFAULT_ANIMATION_BACKGROUND_COLOR) {
// This window is embedded and has an animation background color set on the
// TaskFragment. Pass this color with this window, so the handler can use it as
// the animation background color if needed,
backgroundColor = organizedTf.getAnimationParams()
.getAnimationBackgroundColor();
} else {
// Set background color to Task theme color for activity and embedded
// TaskFragment in case we want to show background during the animation.
final Task parentTask = activityRecord != null
? activityRecord.getTask()
: taskFragment.getTask();
backgroundColor = parentTask.getTaskDescription().getBackgroundColor();
}
// Set to opaque for animation background to prevent it from exposing the blank
// background or content below.
change.setBackgroundColor(ColorUtils.setAlphaComponent(backgroundColor, 255));
}
change.setRotation(info.mRotation, endRotation);
if (info.mSnapshot != null) {
change.setSnapshot(info.mSnapshot, info.mSnapshotLuma);
}
out.addChange(change);
}
TransitionInfo.AnimationOptions animOptions = null;
// Check if the top-most app is an activity (ie. activity->activity). If so, make sure to
// honor its custom transition options.
WindowContainer<?> topApp = null;
for (int i = 0; i < sortedTargets.size(); i++) {
if (isWallpaper(sortedTargets.get(i).mContainer)) continue;
topApp = sortedTargets.get(i).mContainer;
break;
}
if (topApp.asActivityRecord() != null) {
final ActivityRecord topActivity = topApp.asActivityRecord();
animOptions = addCustomActivityTransition(topActivity, true/* open */, null);
animOptions = addCustomActivityTransition(topActivity, false/* open */, animOptions);
}
final WindowManager.LayoutParams animLp =
getLayoutParamsForAnimationsStyle(type, sortedTargets);
if (animLp != null && animLp.type != TYPE_APPLICATION_STARTING
&& animLp.windowAnimations != 0) {
// Don't send animation options if no windowAnimations have been set or if the we are
// running an app starting animation, in which case we don't want the app to be able to
// change its animation directly.
if (animOptions != null) {
animOptions.addOptionsFromLayoutParameters(animLp);
} else {
animOptions = TransitionInfo.AnimationOptions
.makeAnimOptionsFromLayoutParameters(animLp);
}
}
if (animOptions != null) {
out.setAnimationOptions(animOptions);
}
return out;
}
static TransitionInfo.AnimationOptions addCustomActivityTransition(ActivityRecord topActivity,
boolean open, TransitionInfo.AnimationOptions animOptions) {
final ActivityRecord.CustomAppTransition customAnim =
topActivity.getCustomAnimation(open);
if (customAnim != null) {
if (animOptions == null) {
animOptions = TransitionInfo.AnimationOptions
.makeCommonAnimOptions(topActivity.packageName);
}
animOptions.addCustomActivityTransition(open, customAnim.mEnterAnim,
customAnim.mExitAnim, customAnim.mBackgroundColor);
}
return animOptions;
}
private static void setEndFixedRotationIfNeeded(@NonNull TransitionInfo.Change change,
@NonNull Task task, @NonNull ActivityRecord taskTopRunning) {
if (!taskTopRunning.isVisibleRequested()) {
// Fixed rotation only applies to opening or changing activity.
return;
}
if (task.inMultiWindowMode() && taskTopRunning.inMultiWindowMode()) {
// Display won't be rotated for multi window Task, so the fixed rotation won't be
// applied. This can happen when the windowing mode is changed before the previous
// fixed rotation is applied. Check both task and activity because the activity keeps
// fullscreen mode when the task is entering PiP.
return;
}
final int taskRotation = task.getWindowConfiguration().getDisplayRotation();
final int activityRotation = taskTopRunning.getWindowConfiguration()
.getDisplayRotation();
// If the Activity uses fixed rotation, its rotation will be applied to display after
// the current transition is done, while the Task is still in the previous rotation.
if (taskRotation != activityRotation) {
change.setEndFixedRotation(activityRotation);
return;
}
// For example, the task is entering PiP so it no longer decides orientation. If the next
// orientation source (it could be an activity which was behind the PiP or launching to top)
// will change display rotation, then set the fixed rotation hint as well so the animation
// can consider the rotated position.
if (!task.inPinnedWindowingMode() || taskTopRunning.mDisplayContent.inTransition()) {
return;
}
final WindowContainer<?> orientationSource =
taskTopRunning.mDisplayContent.getLastOrientationSource();
if (orientationSource == null) {
return;
}
final int nextRotation = orientationSource.getWindowConfiguration().getDisplayRotation();
if (taskRotation != nextRotation) {
change.setEndFixedRotation(nextRotation);
}
}
/**
* Finds the top-most common ancestor of app targets.
*
* Makes sure that the previous parent is also a descendant to make sure the animation won't
* be covered by other windows below the previous parent. For example, when reparenting an
* activity from PiP Task to split screen Task.
*/
@NonNull
private static WindowContainer<?> findCommonAncestor(
@NonNull ArrayList<ChangeInfo> targets,
@NonNull WindowContainer<?> topApp) {
final int displayId = getDisplayId(topApp);
WindowContainer<?> ancestor = topApp.getParent();
// Go up ancestor parent chain until all targets are descendants. Ancestor should never be
// null because all targets are attached.
for (int i = targets.size() - 1; i >= 0; i--) {
final ChangeInfo change = targets.get(i);
final WindowContainer wc = change.mContainer;
if (isWallpaper(wc) || getDisplayId(wc) != displayId) {
// Skip the non-app window or windows on a different display
continue;
}
// Re-initiate the last parent as the initial ancestor instead of the top target.
// When move a leaf task from organized task to display area, try to keep the transition
// root be the original organized task for close transition animation.
// Otherwise, shell will use wrong root layer to play animation.
// Note: Since the target is sorted, so only need to do this at the lowest target.
if (change.mStartParent != null && wc.getParent() != null
&& change.mStartParent.isAttached() && wc.getParent() != change.mStartParent
&& i == targets.size() - 1) {
final int transitionMode = change.getTransitMode(wc);
if (transitionMode == TRANSIT_CLOSE || transitionMode == TRANSIT_TO_BACK) {
ancestor = change.mStartParent;
continue;
}
}
while (!wc.isDescendantOf(ancestor)) {
ancestor = ancestor.getParent();
}
// Make sure the previous parent is also a descendant to make sure the animation won't
// be covered by other windows below the previous parent. For example, when reparenting
// an activity from PiP Task to split screen Task.
final WindowContainer prevParent = change.mCommonAncestor;
if (prevParent == null || !prevParent.isAttached()) {
continue;
}
while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
ancestor = ancestor.getParent();
}
}
return ancestor;
}
private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type,
ArrayList<ChangeInfo> sortedTargets) {
// Find the layout params of the top-most application window that is part of the
// transition, which is what will control the animation theme.
final ArraySet<Integer> activityTypes = new ArraySet<>();
final int targetCount = sortedTargets.size();
for (int i = 0; i < targetCount; ++i) {
final WindowContainer target = sortedTargets.get(i).mContainer;
if (target.asActivityRecord() != null) {
activityTypes.add(target.getActivityType());
} else if (target.asWindowToken() == null && target.asWindowState() == null) {
// We don't want app to customize animations that are not activity to activity.
// Activity-level transitions can only include activities, wallpaper and subwindows.
// Anything else is not a WindowToken nor a WindowState and is "higher" in the
// hierarchy which means we are no longer in an activity transition.
return null;
}
}
if (activityTypes.isEmpty()) {
// We don't want app to be able to customize transitions that are not activity to
// activity through the layout parameter animation style.
return null;
}
final ActivityRecord animLpActivity =
findAnimLayoutParamsActivityRecord(sortedTargets, type, activityTypes);
final WindowState mainWindow = animLpActivity != null
? animLpActivity.findMainWindow() : null;
return mainWindow != null ? mainWindow.mAttrs : null;
}
private static ActivityRecord findAnimLayoutParamsActivityRecord(
List<ChangeInfo> sortedTargets,
@TransitionType int transit, ArraySet<Integer> activityTypes) {
// Remote animations always win, but fullscreen windows override non-fullscreen windows.
ActivityRecord result = lookForTopWindowWithFilter(sortedTargets,
w -> w.getRemoteAnimationDefinition() != null
&& w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes));
if (result != null) {
return result;
}
result = lookForTopWindowWithFilter(sortedTargets,
w -> w.fillsParent() && w.findMainWindow() != null);
if (result != null) {
return result;
}
return lookForTopWindowWithFilter(sortedTargets, w -> w.findMainWindow() != null);
}
private static ActivityRecord lookForTopWindowWithFilter(List<ChangeInfo> sortedTargets,
Predicate<ActivityRecord> filter) {
final int count = sortedTargets.size();
for (int i = 0; i < count; ++i) {
final WindowContainer target = sortedTargets.get(i).mContainer;
final ActivityRecord activityRecord = target.asTaskFragment() != null
? target.asTaskFragment().getTopNonFinishingActivity()
: target.asActivityRecord();
if (activityRecord != null && filter.test(activityRecord)) {
return activityRecord;
}
}
return null;
}
private static int getTaskRotationAnimation(@NonNull Task task) {
final ActivityRecord top = task.getTopVisibleActivity();
if (top == null) return ROTATION_ANIMATION_UNSPECIFIED;
final WindowState mainWin = top.findMainWindow(false);
if (mainWin == null) return ROTATION_ANIMATION_UNSPECIFIED;
int anim = mainWin.getRotationAnimationHint();
if (anim >= 0) return anim;
anim = mainWin.getAttrs().rotationAnimation;
if (anim != ROTATION_ANIMATION_SEAMLESS) return anim;
if (mainWin != task.mDisplayContent.getDisplayPolicy().getTopFullscreenOpaqueWindow()
|| !top.matchParentBounds()) {
// At the moment, we only support seamless rotation if there is only one window showing.
return ROTATION_ANIMATION_UNSPECIFIED;
}
return mainWin.getAttrs().rotationAnimation;
}
private void validateKeyguardOcclusion() {
if ((mFlags & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) {
mController.mStateValidators.add(
mController.mAtm.mWindowManager.mPolicy::applyKeyguardOcclusionChange);
}
}
/**
* Returns {@code true} if the transition and the corresponding transaction should be applied
* on display thread. Currently, this only checks for display rotation change because the order
* of dispatching the new display info will be after requesting the windows to sync drawing.
* That avoids potential flickering of screen overlays (e.g. cutout, rounded corner). Also,
* because the display thread has a higher priority, it is faster to perform the configuration
* changes and window hierarchy traversal.
*/
boolean shouldApplyOnDisplayThread() {
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final DisplayContent dc = mParticipants.valueAt(i).asDisplayContent();
if (dc == null) continue;
final ChangeInfo changeInfo = mChanges.get(dc);
if (changeInfo != null && changeInfo.mRotation != dc.getRotation()) {
return Looper.myLooper() != mController.mAtm.mWindowManager.mH.getLooper();
}
}
return false;
}
/**
* Applies the new configuration for the changed displays. Returns the activities that should
* check whether to deliver the new configuration to clients.
*/
@Nullable
ArrayList<ActivityRecord> applyDisplayChangeIfNeeded() {
ArrayList<ActivityRecord> activitiesMayChange = null;
for (int i = mParticipants.size() - 1; i >= 0; --i) {
final WindowContainer<?> wc = mParticipants.valueAt(i);
final DisplayContent dc = wc.asDisplayContent();
if (dc == null || !mChanges.get(dc).hasChanged()) continue;
final boolean changed = dc.sendNewConfiguration();
// Set to ready if no other change controls the ready state. But if there is, such as
// if an activity is pausing, it will call setReady(ar, false) and wait for the next
// resumed activity. Then do not set to ready because the transition only contains
// partial participants. Otherwise the transition may only handle HIDE and miss OPEN.
if (!mReadyTrackerOld.mUsed) {
setReady(dc, true);
}
if (!changed) continue;
// If the update is deferred, sendNewConfiguration won't deliver new configuration to
// clients, then it is the caller's responsibility to deliver the changes.
if (mController.mAtm.mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
if (activitiesMayChange == null) {
activitiesMayChange = new ArrayList<>();
}
final ArrayList<ActivityRecord> visibleActivities = activitiesMayChange;
dc.forAllActivities(r -> {
if (r.isVisibleRequested()) {
visibleActivities.add(r);
}
});
}
}
return activitiesMayChange;
}
boolean getLegacyIsReady() {
return isCollecting() && mSyncId >= 0;
}
static void asyncTraceBegin(@NonNull String name, int cookie) {
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, name, cookie);
}
static void asyncTraceEnd(int cookie) {
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_WINDOW_MANAGER, TAG, cookie);
}
@VisibleForTesting
static class ChangeInfo {
private static final int FLAG_NONE = 0;
/**
* When set, the associated WindowContainer has been explicitly requested to be a
* seamless rotation. This is currently only used by DisplayContent during fixed-rotation.
*/
private static final int FLAG_SEAMLESS_ROTATION = 1;
private static final int FLAG_TRANSIENT_LAUNCH = 2;
private static final int FLAG_ABOVE_TRANSIENT_LAUNCH = 4;
/** This container explicitly requested no-animation (usually Activity level). */
private static final int FLAG_CHANGE_NO_ANIMATION = 0x8;
/**
* This container has at-least one child which IS animating (not marked NO_ANIMATION).
* Used during promotion. This trumps `FLAG_NO_ANIMATION` (if both are set).
*/
private static final int FLAG_CHANGE_YES_ANIMATION = 0x10;
/** Whether this change's container moved to the top. */
private static final int FLAG_CHANGE_MOVED_TO_TOP = 0x20;
@IntDef(prefix = { "FLAG_" }, value = {
FLAG_NONE,
FLAG_SEAMLESS_ROTATION,
FLAG_TRANSIENT_LAUNCH,
FLAG_ABOVE_TRANSIENT_LAUNCH,
FLAG_CHANGE_NO_ANIMATION,
FLAG_CHANGE_YES_ANIMATION,
FLAG_CHANGE_MOVED_TO_TOP
})
@Retention(RetentionPolicy.SOURCE)
@interface Flag {}
@NonNull final WindowContainer mContainer;
/**
* "Parent" that is also included in the transition. When populating the parent changes, we
* may skip the intermediate parents, so this may not be the actual parent in the hierarchy.
*/
WindowContainer mEndParent;
/** Actual parent window before change state. */
WindowContainer mStartParent;
/**
* When the window is reparented during the transition, this is the common ancestor window
* of the {@link #mStartParent} and the current parent. This is needed because the
* {@link #mStartParent} may have been detached when the transition starts.
*/
WindowContainer mCommonAncestor;
// State tracking
boolean mExistenceChanged = false;
// before change state
boolean mVisible;
int mWindowingMode;
final Rect mAbsoluteBounds = new Rect();
boolean mShowWallpaper;
int mRotation = ROTATION_UNDEFINED;
int mDisplayId = -1;
@ActivityInfo.Config int mKnownConfigChanges;
/** Extra information about this change. */
@Flag int mFlags = FLAG_NONE;
/** Snapshot surface and luma, if relevant. */
SurfaceControl mSnapshot;
float mSnapshotLuma;
/** The mode which is set when the transition is ready. */
@TransitionInfo.TransitionMode
int mReadyMode;
/** The flags which is set when the transition is ready. */
@TransitionInfo.ChangeFlags
int mReadyFlags;
ChangeInfo(@NonNull WindowContainer origState) {
mContainer = origState;
mVisible = origState.isVisibleRequested();
mWindowingMode = origState.getWindowingMode();
mAbsoluteBounds.set(origState.getBounds());
mShowWallpaper = origState.showWallpaper();
mRotation = origState.getWindowConfiguration().getRotation();
mStartParent = origState.getParent();
mDisplayId = getDisplayId(origState);
}
@VisibleForTesting
ChangeInfo(@NonNull WindowContainer container, boolean visible, boolean existChange) {
this(container);
mVisible = visible;
mExistenceChanged = existChange;
mShowWallpaper = false;
}
@Override
public String toString() {
return mContainer.toString();
}
boolean hasChanged() {
// the task including transient launch must promote to root task
if ((mFlags & ChangeInfo.FLAG_TRANSIENT_LAUNCH) != 0
|| (mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) {
return true;
}
// If it's invisible and hasn't changed visibility, always return false since even if
// something changed, it wouldn't be a visible change.
final boolean currVisible = mContainer.isVisibleRequested();
if (currVisible == mVisible && !mVisible) return false;
return currVisible != mVisible
|| mKnownConfigChanges != 0
// if mWindowingMode is 0, this container wasn't attached at collect time, so
// assume no change in windowing-mode.
|| (mWindowingMode != 0 && mContainer.getWindowingMode() != mWindowingMode)
|| !mContainer.getBounds().equals(mAbsoluteBounds)
|| mRotation != mContainer.getWindowConfiguration().getRotation()
|| mDisplayId != getDisplayId(mContainer)
|| (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0;
}
@TransitionInfo.TransitionMode
int getTransitMode(@NonNull WindowContainer wc) {
if ((mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) {
return mExistenceChanged ? TRANSIT_CLOSE : TRANSIT_TO_BACK;
}
final boolean nowVisible = wc.isVisibleRequested();
if (nowVisible == mVisible) {
return TRANSIT_CHANGE;
}
if (mExistenceChanged) {
return nowVisible ? TRANSIT_OPEN : TRANSIT_CLOSE;
} else {
return nowVisible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK;
}
}
@TransitionInfo.ChangeFlags
int getChangeFlags(@NonNull WindowContainer wc) {
int flags = 0;
if (mShowWallpaper || wc.showWallpaper()) {
flags |= FLAG_SHOW_WALLPAPER;
}
if (isTranslucent(wc)) {
flags |= FLAG_TRANSLUCENT;
}
if (wc.mWmService.mAtmService.mBackNavigationController.isMonitorTransitionTarget(wc)) {
flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
}
final Task task = wc.asTask();
if (task != null) {
final ActivityRecord topActivity = task.getTopNonFinishingActivity();
if (topActivity != null) {
if (topActivity.mStartingData != null
&& topActivity.mStartingData.hasImeSurface()) {
flags |= FLAG_WILL_IME_SHOWN;
}
if (topActivity.mLaunchTaskBehind) {
Slog.e(TAG, "Unexpected launch-task-behind operation in shell transition");
flags |= FLAG_TASK_LAUNCHING_BEHIND;
}
}
if (task.voiceSession != null) {
flags |= FLAG_IS_VOICE_INTERACTION;
}
}
Task parentTask = null;
final ActivityRecord record = wc.asActivityRecord();
if (record != null) {
parentTask = record.getTask();
if (record.mVoiceInteraction) {
flags |= FLAG_IS_VOICE_INTERACTION;
}
flags |= record.mTransitionChangeFlags;
}
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && task == null) {
parentTask = taskFragment.getTask();
}
if (parentTask != null) {
if (parentTask.forAllLeafTaskFragments(TaskFragment::isEmbedded)) {
// Whether this is in a Task with embedded activity.
flags |= FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
}
if (parentTask.forAllActivities(ActivityRecord::hasStartingWindow)) {
// The starting window should cover all windows inside the leaf Task.
flags |= FLAG_IS_BEHIND_STARTING_WINDOW;
}
if (isWindowFillingTask(wc, parentTask)) {
// Whether the container fills its parent Task bounds.
flags |= FLAG_FILLS_TASK;
}
} else {
final DisplayContent dc = wc.asDisplayContent();
if (dc != null) {
flags |= FLAG_IS_DISPLAY;
if (dc.hasAlertWindowSurfaces()) {
flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS;
}
} else if (isWallpaper(wc)) {
flags |= FLAG_IS_WALLPAPER;
} else if (isInputMethod(wc)) {
flags |= FLAG_IS_INPUT_METHOD;
} else {
// In this condition, the wc can only be WindowToken or DisplayArea.
final int type = wc.getWindowType();
if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
&& type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
flags |= TransitionInfo.FLAG_IS_SYSTEM_WINDOW;
}
}
}
if ((mFlags & FLAG_CHANGE_NO_ANIMATION) != 0
&& (mFlags & FLAG_CHANGE_YES_ANIMATION) == 0) {
flags |= FLAG_NO_ANIMATION;
}
if ((mFlags & FLAG_CHANGE_MOVED_TO_TOP) != 0) {
flags |= FLAG_MOVED_TO_TOP;
}
return flags;
}
/** Whether the container fills its parent Task bounds before and after the transition. */
private boolean isWindowFillingTask(@NonNull WindowContainer wc, @NonNull Task parentTask) {
final Rect taskBounds = parentTask.getBounds();
final int taskWidth = taskBounds.width();
final int taskHeight = taskBounds.height();
final Rect startBounds = mAbsoluteBounds;
final Rect endBounds = wc.getBounds();
// Treat it as filling the task if it is not visible.
final boolean isInvisibleOrFillingTaskBeforeTransition = !mVisible
|| (taskWidth == startBounds.width() && taskHeight == startBounds.height());
final boolean isInVisibleOrFillingTaskAfterTransition = !wc.isVisibleRequested()
|| (taskWidth == endBounds.width() && taskHeight == endBounds.height());
return isInvisibleOrFillingTaskBeforeTransition
&& isInVisibleOrFillingTaskAfterTransition;
}
}
/**
* This transition will be considered not-ready until a corresponding call to
* {@link #continueTransitionReady}
*/
void deferTransitionReady() {
++mReadyTrackerOld.mDeferReadyDepth;
// Make sure it wait until #continueTransitionReady() is called.
mSyncEngine.setReady(mSyncId, false);
}
/** This undoes one call to {@link #deferTransitionReady}. */
void continueTransitionReady() {
--mReadyTrackerOld.mDeferReadyDepth;
// Apply ready in case it is waiting for the previous defer call.
applyReady();
}
/**
* Represents a condition that must be met before an associated transition can be considered
* ready.
*
* Expected usage is that a ReadyCondition is created and then attached to a transition's
* ReadyTracker via {@link ReadyTracker#add}. After that, it is expected to monitor the state
* of the system and when the condition it represents is met, it will call
* {@link ReadyTracker#meet}.
*
* This base class is a simple explicit, named condition. A caller will create/attach the
* condition and then explicitly call {@link #meet} on it (which internally calls
* {@link ReadyTracker#meet}.
*
* Example:
* <pre>
* ReadyCondition myCondition = new ReadyCondition("my condition");
* transitionController.waitFor(myCondition);
* ... Some operations ...
* myCondition.meet();
* </pre>
*/
static class ReadyCondition {
final String mName;
/** Just used for debugging */
final Object mDebugTarget;
ReadyTracker mTracker;
boolean mMet = false;
/** If set (non-null), then this is met by another reason besides state (eg. timeout). */
String mAlternate = null;
ReadyCondition(@NonNull String name) {
mName = name;
mDebugTarget = null;
}
ReadyCondition(@NonNull String name, @Nullable Object debugTarget) {
mName = name;
mDebugTarget = debugTarget;
}
protected String getDebugRep() {
if (mDebugTarget != null) {
return mName + ":" + mDebugTarget;
}
return mName;
}
@Override
public String toString() {
return "{" + getDebugRep() + (mAlternate != null ? " (" + mAlternate + ")" : "") + "}";
}
/**
* Instructs this condition to start tracking system state to detect when this is met.
* Don't call this directly; it is called when this object is attached to a transition's
* ready-tracker.
*/
void startTracking() {
}
/**
* Immediately consider this condition met by an alternative reason (one which doesn't
* match the normal intent of this condition -- eg. a timeout).
*/
void meetAlternate(@NonNull String reason) {
if (mMet) return;
mAlternate = reason;
meet();
}
/** Immediately consider this condition met. */
void meet() {
if (mMet) return;
if (mTracker == null) {
throw new IllegalStateException("Can't meet a condition before it is waited on");
}
mTracker.meet(this);
}
}
static class ReadyTracker {
/**
* Used as a place-holder in situations where the transition system isn't active (such as
* early-boot, mid shell crash/recovery, or when using legacy).
*/
static final ReadyTracker NULL_TRACKER = new ReadyTracker(null);
private final Transition mTransition;
/** List of conditions that are still being waited on. */
final ArrayList<ReadyCondition> mConditions = new ArrayList<>();
/** List of already-met conditions. Fully-qualified for debugging. */
final ArrayList<ReadyCondition> mMet = new ArrayList<>();
ReadyTracker(Transition transition) {
mTransition = transition;
}
void add(@NonNull ReadyCondition condition) {
if (mTransition == null || !mTransition.mController.useFullReadyTracking()) {
condition.mTracker = NULL_TRACKER;
return;
}
mConditions.add(condition);
condition.mTracker = this;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Add condition %s for #%d",
condition, mTransition.mSyncId);
condition.startTracking();
}
void meet(@NonNull ReadyCondition condition) {
if (mTransition == null || !mTransition.mController.useFullReadyTracking()) return;
if (mTransition.mState >= STATE_PLAYING) {
Slog.w(TAG, "#%d: Condition met too late, already in state=" + mTransition.mState
+ ": " + condition);
return;
}
if (!mConditions.remove(condition)) {
if (mMet.contains(condition)) {
throw new IllegalStateException("Can't meet the same condition more than once: "
+ condition + " #" + mTransition.mSyncId);
} else {
throw new IllegalArgumentException("Can't meet a condition that isn't being "
+ "waited on: " + condition + " in #" + mTransition.mSyncId);
}
}
condition.mMet = true;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Met condition %s for #%d (%d"
+ " left)", condition, mTransition.mSyncId, mConditions.size());
mMet.add(condition);
mTransition.applyReady();
}
boolean isReady() {
return mConditions.isEmpty() && !mMet.isEmpty();
}
}
/**
* The transition sync mechanism has 2 parts:
* 1. Whether all WM operations for a particular transition are "ready" (eg. did the app
* launch or stop or get a new configuration?).
* 2. Whether all the windows involved have finished drawing their final-state content.
*
* A transition animation can play once both parts are complete. This ready-tracker keeps track
* of part (1). Currently, WM code assumes that "readiness" (part 1) is grouped. This means that
* even if the WM operations in one group are ready, the whole transition itself may not be
* ready if there are WM operations still pending in another group. This class helps keep track
* of readiness across the multiple groups. Currently, we assume that each display is a group
* since that is how it has been until now.
*/
private static class ReadyTrackerOld {
private final ArrayMap<WindowContainer, Boolean> mReadyGroups = new ArrayMap<>();
/**
* Ensures that this doesn't report as allReady before it has been used. This is needed
* in very niche cases where a transition is a no-op (nothing has been collected) but we
* still want to be marked ready (via. setAllReady).
*/
private boolean mUsed = false;
/**
* If true, this overrides all ready groups and reports ready. Used by shell-initiated
* transitions via {@link #setAllReady()}.
*/
private boolean mReadyOverride = false;
/**
* When non-zero, this transition is forced not-ready (even over setAllReady()). Use this
* (via deferTransitionReady/continueTransitionReady) for situations where we want to do
* bulk operations which could trigger surface-placement but the existing ready-state
* isn't known.
*/
private int mDeferReadyDepth = 0;
/**
* Adds a ready-group. Any setReady calls in this subtree will be tracked together. For
* now these are only DisplayContents.
*/
void addGroup(WindowContainer wc) {
if (mReadyGroups.containsKey(wc)) {
return;
}
mReadyGroups.put(wc, false);
}
/**
* Sets a group's ready state.
* @param wc Any container within a group's subtree. Used to identify the ready-group.
*/
void setReadyFrom(WindowContainer wc, boolean ready) {
mUsed = true;
WindowContainer current = wc;
while (current != null) {
if (isReadyGroup(current)) {
mReadyGroups.put(current, ready);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Setting Ready-group to"
+ " %b. group=%s from %s", ready, current, wc);
break;
}
current = current.getParent();
}
}
/** Marks this as ready regardless of individual groups. */
void setAllReady() {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Setting allReady override");
mUsed = true;
mReadyOverride = true;
}
/** @return true if all tracked subtrees are ready. */
boolean allReady() {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " allReady query: used=%b "
+ "override=%b defer=%d states=[%s]", mUsed, mReadyOverride, mDeferReadyDepth,
groupsToString());
// If the readiness has never been touched, mUsed will be false. We never want to
// consider a transition ready if nothing has been reported on it.
if (!mUsed) return false;
// If we are deferring readiness, we never report ready. This is usually temporary.
if (mDeferReadyDepth > 0) return false;
// Next check all the ready groups to see if they are ready. We can short-cut this if
// ready-override is set (which is treated as "everything is marked ready").
if (mReadyOverride) return true;
for (int i = mReadyGroups.size() - 1; i >= 0; --i) {
final WindowContainer wc = mReadyGroups.keyAt(i);
if (!wc.isAttached() || !wc.isVisibleRequested()) continue;
if (!mReadyGroups.valueAt(i)) return false;
}
return true;
}
private String groupsToString() {
StringBuilder b = new StringBuilder();
for (int i = 0; i < mReadyGroups.size(); ++i) {
if (i != 0) b.append(',');
b.append(mReadyGroups.keyAt(i)).append(':')
.append(mReadyGroups.valueAt(i));
}
return b.toString();
}
}
/**
* The container to represent the depth relation for calculating transition targets. The window
* container with larger depth is put at larger index. For the same depth, higher z-order has
* larger index.
*/
private static class Targets {
/** All targets. Its keys (depth) are sorted in ascending order naturally. */
final SparseArray<ChangeInfo> mArray = new SparseArray<>();
/** The targets which were represented by their parent. */
private ArrayList<ChangeInfo> mRemovedTargets;
private int mDepthFactor;
void add(ChangeInfo target) {
// The number of slots per depth is larger than the total number of window container,
// so the depth score (key) won't have collision.
if (mDepthFactor == 0) {
mDepthFactor = target.mContainer.mWmService.mRoot.getTreeWeight() + 1;
}
int score = target.mContainer.getPrefixOrderIndex();
WindowContainer<?> wc = target.mContainer;
while (wc != null) {
final WindowContainer<?> parent = wc.getParent();
if (parent != null) {
score += mDepthFactor;
}
wc = parent;
}
mArray.put(score, target);
}
void remove(int index) {
final ChangeInfo removingTarget = mArray.valueAt(index);
mArray.removeAt(index);
if (mRemovedTargets == null) {
mRemovedTargets = new ArrayList<>();
}
mRemovedTargets.add(removingTarget);
}
boolean wasParticipated(ChangeInfo wc) {
return mArray.indexOfValue(wc) >= 0
|| (mRemovedTargets != null && mRemovedTargets.contains(wc));
}
/** Returns the target list sorted by z-order in ascending order (index 0 is top). */
ArrayList<ChangeInfo> getListSortedByZ() {
final SparseArray<ChangeInfo> arrayByZ = new SparseArray<>(mArray.size());
for (int i = mArray.size() - 1; i >= 0; --i) {
final int zOrder = mArray.keyAt(i) % mDepthFactor;
arrayByZ.put(zOrder, mArray.valueAt(i));
}
final ArrayList<ChangeInfo> sortedTargets = new ArrayList<>(arrayByZ.size());
for (int i = arrayByZ.size() - 1; i >= 0; --i) {
sortedTargets.add(arrayByZ.valueAt(i));
}
return sortedTargets;
}
}
/**
* Interface for freezing a container's content during sync preparation. Really just one impl
* but broken into an interface for testing (since you can't take screenshots in unit tests).
*/
interface IContainerFreezer {
/**
* Makes sure a particular window is "frozen" for the remainder of a sync.
*
* @return whether the freeze was successful. It fails if `wc` is already in a frozen window
* or is not visible/ready.
*/
boolean freeze(@NonNull WindowContainer wc, @NonNull Rect bounds);
/** Populates `t` with operations that clean-up any state created to set-up the freeze. */
void cleanUp(SurfaceControl.Transaction t);
}
/**
* Freezes container content by taking a screenshot. Because screenshots are heavy, usage of
* any container "freeze" is currently explicit. WM code needs to be prudent about which
* containers to freeze.
*/
@VisibleForTesting
private class ScreenshotFreezer implements IContainerFreezer {
/** Keeps track of which windows are frozen. Not all frozen windows have snapshots. */
private final ArraySet<WindowContainer> mFrozen = new ArraySet<>();
/** Takes a screenshot and puts it at the top of the container's surface. */
@Override
public boolean freeze(@NonNull WindowContainer wc, @NonNull Rect bounds) {
if (!wc.isVisibleRequested()) return false;
// Check if any parents have already been "frozen". If so, `wc` is already part of that
// snapshot, so just skip it.
for (WindowContainer p = wc; p != null; p = p.getParent()) {
if (mFrozen.contains(p)) return false;
}
if (mIsSeamlessRotation) {
WindowState top = wc.getDisplayContent() == null ? null
: wc.getDisplayContent().getDisplayPolicy().getTopFullscreenOpaqueWindow();
if (top != null && (top == wc || top.isDescendantOf(wc))) {
// Don't use screenshots for seamless windows: these will use BLAST even if not
// BLAST mode.
mFrozen.add(wc);
return true;
}
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Screenshotting %s [%s]",
wc.toString(), bounds.toString());
Rect cropBounds = new Rect(bounds);
cropBounds.offsetTo(0, 0);
final boolean isDisplayRotation = wc.asDisplayContent() != null
&& wc.asDisplayContent().isRotationChanging();
ScreenCapture.LayerCaptureArgs captureArgs =
new ScreenCapture.LayerCaptureArgs.Builder(wc.getSurfaceControl())
.setSourceCrop(cropBounds)
.setCaptureSecureLayers(true)
.setAllowProtected(true)
.setHintForSeamlessTransition(isDisplayRotation)
.build();
ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
ScreenCapture.captureLayers(captureArgs);
final HardwareBuffer buffer = screenshotBuffer == null ? null
: screenshotBuffer.getHardwareBuffer();
if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
// This can happen when display is not ready.
Slog.w(TAG, "Failed to capture screenshot for " + wc);
return false;
}
// Some tests may check the name "RotationLayer" to detect display rotation.
final String name = isDisplayRotation ? "RotationLayer" : "transition snapshot: " + wc;
SurfaceControl snapshotSurface = wc.makeAnimationLeash()
.setName(name)
.setOpaque(wc.fillsParent())
.setParent(wc.getSurfaceControl())
.setSecure(screenshotBuffer.containsSecureLayers())
.setCallsite("Transition.ScreenshotSync")
.setBLASTLayer()
.build();
mFrozen.add(wc);
final ChangeInfo changeInfo = Objects.requireNonNull(mChanges.get(wc));
changeInfo.mSnapshot = snapshotSurface;
if (changeInfo.mRotation != wc.mDisplayContent.getRotation()) {
// This isn't cheap, so only do it for rotation change.
changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
buffer, screenshotBuffer.getColorSpace());
}
SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
TransitionAnimation.configureScreenshotLayer(t, snapshotSurface, screenshotBuffer);
t.show(snapshotSurface);
// Place it on top of anything else in the container.
t.setLayer(snapshotSurface, Integer.MAX_VALUE);
t.apply();
t.close();
buffer.close();
// Detach the screenshot on the sync transaction (the screenshot is just meant to
// freeze the window until the sync transaction is applied (with all its other
// corresponding changes), so this is how we unfreeze it.
wc.getSyncTransaction().reparent(snapshotSurface, null /* newParent */);
return true;
}
@Override
public void cleanUp(SurfaceControl.Transaction t) {
for (int i = 0; i < mFrozen.size(); ++i) {
SurfaceControl snap =
Objects.requireNonNull(mChanges.get(mFrozen.valueAt(i))).mSnapshot;
// May be null if it was frozen via BLAST override.
if (snap == null) continue;
t.reparent(snap, null /* newParent */);
}
}
}
private static class Token extends Binder {
final WeakReference<Transition> mTransition;
Token(Transition transition) {
mTransition = new WeakReference<>(transition);
}
@Override
public String toString() {
return "Token{" + Integer.toHexString(System.identityHashCode(this)) + " "
+ mTransition.get() + "}";
}
}
}