blob: 1c6a412e5450314462da82450207b55041f73c3e [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.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_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.IApplicationThread;
import android.app.WindowConfiguration;
import android.graphics.Rect;
import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ITransitionMetricsReporter;
import android.window.ITransitionPlayer;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.FgThread;
import java.util.ArrayList;
import java.util.function.LongConsumer;
/**
* Handles all the aspects of recording (collecting) and synchronizing transitions. This is only
* concerned with the WM changes. The actual animations are handled by the Player.
*
* Currently, only 1 transition can be the primary "collector" at a time. This is because WM changes
* are still performed in a "global" manner. However, collecting can actually be broken into
* two phases:
* 1. Actually making WM changes and recording the participating containers.
* 2. Waiting for the participating containers to become ready (eg. redrawing content).
* Because (2) takes most of the time AND doesn't change WM, we can actually have multiple
* transitions in phase (2) concurrently with one in phase (1). We refer to this arrangement as
* "parallel" collection even though there is still only ever 1 transition actually able to gain
* participants.
*
* Parallel collection happens when the "primary collector" has finished "setup" (phase 1) and is
* just waiting. At this point, another transition can start collecting. When this happens, the
* first transition is moved to a "waiting" list and the new transition becomes the "primary
* collector". If at any time, the "primary collector" moves to playing before one of the waiting
* transitions, then the first waiting transition will move back to being the "primary collector".
* This maintains the "global"-like abstraction that the rest of WM currently expects.
*
* When a transition move-to-playing, we check it against all other playing transitions. If it
* doesn't overlap with them, it can also animate in parallel. In this case it will be assigned a
* new "track". "tracks" are a way to communicate to the player about which transitions need to be
* played serially with each-other. So, if we find that a transition overlaps with other transitions
* in one track, the transition will be assigned to that track. If, however, the transition overlaps
* with transition in >1 track, we will actually just mark it as SYNC meaning it can't actually
* play until all prior transition animations finish. This is heavy-handed because it is a fallback
* situation and supporting something fancier would be unnecessarily complicated.
*/
class TransitionController {
private static final String TAG = "TransitionController";
/** Whether to use shell-transitions rotation instead of fixed-rotation. */
private static final boolean SHELL_TRANSITIONS_ROTATION =
SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
/** Which sync method to use for transition syncs. */
static final int SYNC_METHOD =
android.os.SystemProperties.getBoolean("persist.wm.debug.shell_transit_blast", false)
? BLASTSyncEngine.METHOD_BLAST : BLASTSyncEngine.METHOD_NONE;
/** The same as legacy APP_TRANSITION_TIMEOUT_MS. */
private static final int DEFAULT_TIMEOUT_MS = 5000;
/** Less duration for CHANGE type because it does not involve app startup. */
private static final int CHANGE_TIMEOUT_MS = 2000;
// State constants to line-up with legacy app-transition proto expectations.
private static final int LEGACY_STATE_IDLE = 0;
private static final int LEGACY_STATE_READY = 1;
private static final int LEGACY_STATE_RUNNING = 2;
private ITransitionPlayer mTransitionPlayer;
final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter();
private WindowProcessController mTransitionPlayerProc;
final ActivityTaskManagerService mAtm;
BLASTSyncEngine mSyncEngine;
final RemotePlayer mRemotePlayer;
SnapshotController mSnapshotController;
TransitionTracer mTransitionTracer;
private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners =
new ArrayList<>();
/**
* List of runnables to run when there are no ongoing transitions. Use this for state-validation
* checks (eg. to recover from incomplete states). Eventually this should be removed.
*/
final ArrayList<Runnable> mStateValidators = new ArrayList<>();
/**
* List of activity-records whose visibility changed outside the main/tracked part of a
* transition (eg. in the finish-transaction). These will be checked when idle to recover from
* degenerate states.
*/
final ArrayList<ActivityRecord> mValidateCommitVis = new ArrayList<>();
/**
* Currently playing transitions (in the order they were started). When finished, records are
* removed from this list.
*/
private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>();
int mTrackCount = 0;
/** The currently finishing transition. */
Transition mFinishingTransition;
/**
* The windows that request to be invisible while it is in transition. After the transition
* is finished and the windows are no longer animating, their surfaces will be destroyed.
*/
final ArrayList<WindowState> mAnimatingExitWindows = new ArrayList<>();
final Lock mRunningLock = new Lock();
private final IBinder.DeathRecipient mTransitionPlayerDeath;
static class QueuedTransition {
final Transition mTransition;
final OnStartCollect mOnStartCollect;
final BLASTSyncEngine.SyncGroup mLegacySync;
QueuedTransition(Transition transition, OnStartCollect onStartCollect) {
mTransition = transition;
mOnStartCollect = onStartCollect;
mLegacySync = null;
}
QueuedTransition(BLASTSyncEngine.SyncGroup legacySync, OnStartCollect onStartCollect) {
mTransition = null;
mOnStartCollect = onStartCollect;
mLegacySync = legacySync;
}
}
private final ArrayList<QueuedTransition> mQueuedTransitions = new ArrayList<>();
/**
* The transition currently being constructed (collecting participants). Unless interrupted,
* all WM changes will go into this.
*/
private Transition mCollectingTransition = null;
/**
* The transitions that are complete but still waiting for participants to become ready
*/
final ArrayList<Transition> mWaitingTransitions = new ArrayList<>();
/**
* The (non alwaysOnTop) tasks which were reported as on-top of their display most recently
* within a cluster of simultaneous transitions. If tasks are nested, all the tasks that are
* parents of the on-top task are also included. This is used to decide which transitions
* report which on-top changes.
*/
final SparseArray<ArrayList<Task>> mLatestOnTopTasksReported = new SparseArray<>();
/**
* `true` when building surface layer order for the finish transaction. We want to prevent
* wm from touching z-order of surfaces during transitions, but we still need to be able to
* calculate the layers for the finishTransaction. So, when assigning layers into the finish
* transaction, set this to true so that the {@link canAssignLayers} will allow it.
*/
boolean mBuildingFinishLayers = false;
private boolean mAnimatingState = false;
final Handler mLoggerHandler = FgThread.getHandler();
/**
* {@code true} While this waits for the display to become enabled (during boot). While waiting
* for the display, all core-initiated transitions will be "local".
* Note: This defaults to false so that it doesn't interfere with unit tests.
*/
boolean mIsWaitingForDisplayEnabled = false;
TransitionController(ActivityTaskManagerService atm) {
mAtm = atm;
mRemotePlayer = new RemotePlayer(atm);
mTransitionPlayerDeath = () -> {
synchronized (mAtm.mGlobalLock) {
detachPlayer();
}
};
}
void setWindowManager(WindowManagerService wms) {
mSnapshotController = wms.mSnapshotController;
mTransitionTracer = wms.mTransitionTracer;
mIsWaitingForDisplayEnabled = !wms.mDisplayEnabled;
registerLegacyListener(wms.mActivityManagerAppTransitionNotifier);
setSyncEngine(wms.mSyncEngine);
}
@VisibleForTesting
void setSyncEngine(BLASTSyncEngine syncEngine) {
mSyncEngine = syncEngine;
// Check the queue whenever the sync-engine becomes idle.
mSyncEngine.addOnIdleListener(this::tryStartCollectFromQueue);
}
private void detachPlayer() {
if (mTransitionPlayer == null) return;
// Immediately set to null so that nothing inadvertently starts/queues.
mTransitionPlayer = null;
// Clean-up/finish any playing transitions.
for (int i = 0; i < mPlayingTransitions.size(); ++i) {
mPlayingTransitions.get(i).cleanUpOnFailure();
}
mPlayingTransitions.clear();
// Clean up waiting transitions first since they technically started first.
for (int i = 0; i < mWaitingTransitions.size(); ++i) {
mWaitingTransitions.get(i).abort();
}
mWaitingTransitions.clear();
if (mCollectingTransition != null) {
mCollectingTransition.abort();
}
mTransitionPlayerProc = null;
mRemotePlayer.clear();
mRunningLock.doNotifyLocked();
}
/** @see #createTransition(int, int) */
@NonNull
Transition createTransition(int type) {
return createTransition(type, 0 /* flags */);
}
/**
* Creates a transition. It can immediately collect participants.
*/
@NonNull
private Transition createTransition(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags) {
if (mTransitionPlayer == null) {
throw new IllegalStateException("Shell Transitions not enabled");
}
if (mCollectingTransition != null) {
throw new IllegalStateException("Trying to directly start transition collection while "
+ " collection is already ongoing. Use {@link #startCollectOrQueue} if"
+ " possible.");
}
Transition transit = new Transition(type, flags, this, mSyncEngine);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s", transit);
moveToCollecting(transit);
return transit;
}
/** Starts Collecting */
void moveToCollecting(@NonNull Transition transition) {
if (mCollectingTransition != null) {
throw new IllegalStateException("Simultaneous transition collection not supported.");
}
if (mTransitionPlayer == null) {
// If sysui has been killed (by a test) or crashed, we can temporarily have no player
// In this case, abort the transition.
transition.abort();
return;
}
mCollectingTransition = transition;
// Distinguish change type because the response time is usually expected to be not too long.
final long timeoutMs =
transition.mType == TRANSIT_CHANGE ? CHANGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
mCollectingTransition.startCollecting(timeoutMs);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Start collecting in Transition: %s",
mCollectingTransition);
dispatchLegacyAppTransitionPending();
}
void registerTransitionPlayer(@Nullable ITransitionPlayer player,
@Nullable WindowProcessController playerProc) {
try {
// Note: asBinder() can be null if player is same process (likely in a test).
if (mTransitionPlayer != null) {
if (mTransitionPlayer.asBinder() != null) {
mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
}
detachPlayer();
}
if (player.asBinder() != null) {
player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
}
mTransitionPlayer = player;
mTransitionPlayerProc = playerProc;
} catch (RemoteException e) {
throw new RuntimeException("Unable to set transition player");
}
}
@Nullable ITransitionPlayer getTransitionPlayer() {
return mTransitionPlayer;
}
boolean isShellTransitionsEnabled() {
return mTransitionPlayer != null;
}
/** @return {@code true} if using shell-transitions rotation instead of fixed-rotation. */
boolean useShellTransitionsRotation() {
return isShellTransitionsEnabled() && SHELL_TRANSITIONS_ROTATION;
}
/**
* @return {@code true} if transition is actively collecting changes. This is {@code false}
* once a transition is playing
*/
boolean isCollecting() {
return mCollectingTransition != null;
}
/**
* @return the collecting transition. {@code null} if there is no collecting transition.
*/
@Nullable
Transition getCollectingTransition() {
return mCollectingTransition;
}
/**
* @return the collecting transition sync Id. This should only be called when there is a
* collecting transition.
*/
int getCollectingTransitionId() {
if (mCollectingTransition == null) {
throw new IllegalStateException("There is no collecting transition");
}
return mCollectingTransition.getSyncId();
}
/**
* @return {@code true} if transition is actively collecting changes and `wc` is one of them.
* This is {@code false} once a transition is playing.
*/
boolean isCollecting(@NonNull WindowContainer wc) {
if (mCollectingTransition == null) return false;
if (mCollectingTransition.mParticipants.contains(wc)) return true;
for (int i = 0; i < mWaitingTransitions.size(); ++i) {
if (mWaitingTransitions.get(i).mParticipants.contains(wc)) return true;
}
return false;
}
/**
* @return {@code true} if transition is actively collecting changes and `wc` is one of them
* or a descendant of one of them. {@code false} once playing.
*/
boolean inCollectingTransition(@NonNull WindowContainer wc) {
if (!isCollecting()) return false;
if (mCollectingTransition.isInTransition(wc)) return true;
for (int i = 0; i < mWaitingTransitions.size(); ++i) {
if (mWaitingTransitions.get(i).isInTransition(wc)) return true;
}
return false;
}
/**
* @return {@code true} if transition is actively playing. This is not necessarily {@code true}
* during collection.
*/
boolean isPlaying() {
return !mPlayingTransitions.isEmpty();
}
/**
* @return {@code true} if one of the playing transitions contains `wc`.
*/
boolean inPlayingTransition(@NonNull WindowContainer wc) {
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isInTransition(wc)) return true;
}
return false;
}
/** Returns {@code true} if the finishing transition contains `wc`. */
boolean inFinishingTransition(WindowContainer<?> wc) {
return mFinishingTransition != null && mFinishingTransition.isInTransition(wc);
}
/** @return {@code true} if a transition is running */
boolean inTransition() {
// TODO(shell-transitions): eventually properly support multiple
return isCollecting() || isPlaying() || !mQueuedTransitions.isEmpty();
}
/** @return {@code true} if a transition is running in a participant subtree of wc */
boolean inTransition(@NonNull WindowContainer wc) {
return inCollectingTransition(wc) || inPlayingTransition(wc);
}
/** Returns {@code true} if the id matches a collecting or playing transition. */
boolean inTransition(int syncId) {
if (mCollectingTransition != null && mCollectingTransition.getSyncId() == syncId) {
return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).getSyncId() == syncId) {
return true;
}
}
return false;
}
/** @return {@code true} if wc is in a participant subtree */
boolean isTransitionOnDisplay(@NonNull DisplayContent dc) {
if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) {
return true;
}
for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
if (mWaitingTransitions.get(i).isOnDisplay(dc)) return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isOnDisplay(dc)) return true;
}
return false;
}
boolean isTransientHide(@NonNull Task task) {
if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
return true;
}
for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
if (mWaitingTransitions.get(i).isInTransientHide(task)) return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
}
return false;
}
/**
* During transient-launch, the "behind" app should retain focus during the transition unless
* something takes focus from it explicitly (eg. by calling ATMS.setFocusedTask or by another
* transition interrupting this one.
*
* The rules around this are basically: if there is exactly one active transition and `wc` is
* the "behind" of a transient launch, then it can retain focus.
*/
boolean shouldKeepFocus(@NonNull WindowContainer wc) {
if (mCollectingTransition != null) {
if (!mPlayingTransitions.isEmpty()) return false;
return mCollectingTransition.isInTransientHide(wc);
} else if (mPlayingTransitions.size() == 1) {
return mPlayingTransitions.get(0).isInTransientHide(wc);
}
return false;
}
/**
* @return {@code true} if {@param ar} is part of a transient-launch activity in the
* collecting transition.
*/
boolean isTransientCollect(@NonNull ActivityRecord ar) {
return mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar);
}
/**
* @return {@code true} if {@param ar} is part of a transient-launch activity in an active
* transition.
*/
boolean isTransientLaunch(@NonNull ActivityRecord ar) {
if (isTransientCollect(ar)) {
return true;
}
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isTransientLaunch(ar)) return true;
}
return false;
}
/**
* Whether WM can assign layers to window surfaces at this time. This is usually false while
* playing, but can be "opened-up" for certain transition operations like calculating layers
* for finishTransaction.
*/
boolean canAssignLayers(@NonNull WindowContainer wc) {
// Don't build window state into finish transaction in case another window is added or
// removed during transition playing.
if (mBuildingFinishLayers) {
return wc.asWindowState() == null;
}
// Always allow WindowState to assign layers since it won't affect transition.
return wc.asWindowState() != null || !isPlaying();
}
@WindowConfiguration.WindowingMode
int getWindowingModeAtStart(@NonNull WindowContainer wc) {
if (mCollectingTransition == null) return wc.getWindowingMode();
final Transition.ChangeInfo ci = mCollectingTransition.mChanges.get(wc);
if (ci == null) {
// not part of transition, so use current state.
return wc.getWindowingMode();
}
return ci.mWindowingMode;
}
@WindowManager.TransitionType
int getCollectingTransitionType() {
return mCollectingTransition != null ? mCollectingTransition.mType : TRANSIT_NONE;
}
/**
* @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
*/
@Nullable
Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
@NonNull WindowContainer trigger) {
return requestTransitionIfNeeded(type, 0 /* flags */, trigger, trigger /* readyGroupRef */);
}
/**
* @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
*/
@Nullable
Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
@NonNull WindowContainer readyGroupRef) {
return requestTransitionIfNeeded(type, flags, trigger, readyGroupRef,
null /* remoteTransition */, null /* displayChange */);
}
private static boolean isExistenceType(@WindowManager.TransitionType int type) {
return type == TRANSIT_OPEN || type == TRANSIT_CLOSE;
}
/** Sets the sync method for the display change. */
private void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
@NonNull Transition displayTransition, @NonNull DisplayContent displayContent) {
final int startRotation = displayChange.getStartRotation();
final int endRotation = displayChange.getEndRotation();
if (startRotation != endRotation && (startRotation + endRotation) % 2 == 0) {
// 180 degrees rotation change may not change screen size. So the clients may draw
// some frames before and after the display projection transaction is applied by the
// remote player. That may cause some buffers to show in different rotation. So use
// sync method to pause clients drawing until the projection transaction is applied.
mSyncEngine.setSyncMethod(displayTransition.getSyncId(), BLASTSyncEngine.METHOD_BLAST);
}
final Rect startBounds = displayChange.getStartAbsBounds();
final Rect endBounds = displayChange.getEndAbsBounds();
if (startBounds == null || endBounds == null) return;
final int startWidth = startBounds.width();
final int startHeight = startBounds.height();
final int endWidth = endBounds.width();
final int endHeight = endBounds.height();
// This is changing screen resolution. Because the screen decor layers are excluded from
// screenshot, their draw transactions need to run with the start transaction.
if ((endWidth > startWidth) == (endHeight > startHeight)
&& (endWidth != startWidth || endHeight != startHeight)) {
displayContent.forAllWindows(w -> {
if (w.mToken.mRoundedCornerOverlay && w.mHasSurface) {
w.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
}
}, true /* traverseTopToBottom */);
}
}
/**
* If a transition isn't requested yet, creates one and asks the TransitionPlayer (Shell) to
* start it. Collection can start immediately.
* @param trigger if non-null, this is the first container that will be collected
* @param readyGroupRef Used to identify which ready-group this request is for.
* @return the created transition if created or null otherwise.
*/
@Nullable
Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
@NonNull WindowContainer readyGroupRef, @Nullable RemoteTransition remoteTransition,
@Nullable TransitionRequestInfo.DisplayChange displayChange) {
if (mTransitionPlayer == null) {
return null;
}
Transition newTransition = null;
if (isCollecting()) {
if (displayChange != null) {
Slog.e(TAG, "Provided displayChange for a non-new request", new Throwable());
}
// Make the collecting transition wait until this request is ready.
mCollectingTransition.setReady(readyGroupRef, false);
if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
// Add keyguard flag to dismiss keyguard
mCollectingTransition.addFlag(flags);
}
} else {
newTransition = requestStartTransition(createTransition(type, flags),
trigger != null ? trigger.asTask() : null, remoteTransition, displayChange);
if (newTransition != null && displayChange != null && trigger != null
&& trigger.asDisplayContent() != null) {
setDisplaySyncMethod(displayChange, newTransition, trigger.asDisplayContent());
}
}
if (trigger != null) {
if (isExistenceType(type)) {
collectExistenceChange(trigger);
} else {
collect(trigger);
}
}
return newTransition;
}
/** Asks the transition player (shell) to start a created but not yet started transition. */
@NonNull
Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask,
@Nullable RemoteTransition remoteTransition,
@Nullable TransitionRequestInfo.DisplayChange displayChange) {
if (mIsWaitingForDisplayEnabled) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Disabling player for transition"
+ " #%d because display isn't enabled yet", transition.getSyncId());
transition.mIsPlayerEnabled = false;
transition.mLogger.mRequestTimeNs = SystemClock.uptimeNanos();
mAtm.mH.post(() -> mAtm.mWindowOrganizerController.startTransition(
transition.getToken(), null));
return transition;
}
if (mTransitionPlayer == null || transition.isAborted()) {
// Apparently, some tests will kill(and restart) systemui, so there is a chance that
// the player might be transiently null.
if (transition.isCollecting()) {
transition.abort();
}
return transition;
}
try {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Requesting StartTransition: %s", transition);
ActivityManager.RunningTaskInfo info = null;
if (startTask != null) {
info = new ActivityManager.RunningTaskInfo();
startTask.fillTaskInfo(info);
}
final TransitionRequestInfo request = new TransitionRequestInfo(
transition.mType, info, remoteTransition, displayChange);
transition.mLogger.mRequestTimeNs = SystemClock.elapsedRealtimeNanos();
transition.mLogger.mRequest = request;
mTransitionPlayer.requestStartTransition(transition.getToken(), request);
if (remoteTransition != null) {
transition.setRemoteAnimationApp(remoteTransition.getAppThread());
}
} catch (RemoteException e) {
Slog.e(TAG, "Error requesting transition", e);
transition.start();
}
return transition;
}
/**
* Requests transition for a window container which will be removed or invisible.
* @return the new transition if it was created for this request, `null` otherwise.
*/
Transition requestCloseTransitionIfNeeded(@NonNull WindowContainer<?> wc) {
if (mTransitionPlayer == null) return null;
Transition out = null;
if (wc.isVisibleRequested()) {
if (!isCollecting()) {
out = requestStartTransition(createTransition(TRANSIT_CLOSE, 0 /* flags */),
wc.asTask(), null /* remoteTransition */, null /* displayChange */);
}
collectExistenceChange(wc);
} else {
// Removing a non-visible window doesn't require a transition, but if there is one
// collecting, this should be a member just in case.
collect(wc);
}
return out;
}
/** @see Transition#collect */
void collect(@NonNull WindowContainer wc) {
if (mCollectingTransition == null) return;
mCollectingTransition.collect(wc);
}
/** @see Transition#collectExistenceChange */
void collectExistenceChange(@NonNull WindowContainer wc) {
if (mCollectingTransition == null) return;
mCollectingTransition.collectExistenceChange(wc);
}
/** @see Transition#recordTaskOrder */
void recordTaskOrder(@NonNull WindowContainer wc) {
if (mCollectingTransition == null) return;
mCollectingTransition.recordTaskOrder(wc);
}
/**
* Collects the window containers which need to be synced with the changing display area into
* the current collecting transition.
*/
void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc) {
final Transition transition = mCollectingTransition;
if (transition == null || !transition.mParticipants.contains(wc)) return;
transition.collectVisibleChange(wc);
// Collect all visible tasks.
wc.forAllLeafTasks(task -> {
if (task.isVisible()) {
transition.collect(task);
}
}, true /* traverseTopToBottom */);
// Collect all visible non-app windows which need to be drawn before the animation starts.
final DisplayContent dc = wc.asDisplayContent();
if (dc != null) {
final boolean noAsyncRotation = dc.getAsyncRotationController() == null;
wc.forAllWindows(w -> {
if (w.mActivityRecord == null && w.isVisible() && !isCollecting(w.mToken)
&& (noAsyncRotation || !AsyncRotationController.canBeAsync(w.mToken))) {
transition.collect(w.mToken);
}
}, true /* traverseTopToBottom */);
}
}
/**
* 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 (!isCollecting()) return;
mCollectingTransition.collectVisibleChange(wc);
}
/**
* 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 (!isCollecting()) return;
mCollectingTransition.collectReparentChange(wc, newParent);
}
/** @see Transition#mStatusBarTransitionDelay */
void setStatusBarTransitionDelay(long delay) {
if (mCollectingTransition == null) return;
mCollectingTransition.mStatusBarTransitionDelay = delay;
}
/** @see Transition#setOverrideAnimation */
void setOverrideAnimation(TransitionInfo.AnimationOptions options,
@Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
if (mCollectingTransition == null) return;
mCollectingTransition.setOverrideAnimation(options, startCallback, finishCallback);
}
void setNoAnimation(WindowContainer wc) {
if (mCollectingTransition == null) return;
mCollectingTransition.setNoAnimation(wc);
}
/** @see Transition#setReady */
void setReady(WindowContainer wc, boolean ready) {
if (mCollectingTransition == null) return;
mCollectingTransition.setReady(wc, ready);
}
/** @see Transition#setReady */
void setReady(WindowContainer wc) {
setReady(wc, true);
}
/** @see Transition#deferTransitionReady */
void deferTransitionReady() {
if (!isShellTransitionsEnabled()) return;
if (mCollectingTransition == null) {
throw new IllegalStateException("No collecting transition to defer readiness for.");
}
mCollectingTransition.deferTransitionReady();
}
/** @see Transition#continueTransitionReady */
void continueTransitionReady() {
if (!isShellTransitionsEnabled()) return;
if (mCollectingTransition == null) {
throw new IllegalStateException("No collecting transition to defer readiness for.");
}
mCollectingTransition.continueTransitionReady();
}
/** @see Transition#finishTransition */
void finishTransition(Transition record) {
// It is usually a no-op but make sure that the metric consumer is removed.
mTransitionMetricsReporter.reportAnimationStart(record.getToken(), 0 /* startTime */);
// It is a no-op if the transition did not change the display.
mAtm.endLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
if (!mPlayingTransitions.contains(record)) {
Slog.e(TAG, "Trying to finish a non-playing transition " + record);
return;
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record);
mPlayingTransitions.remove(record);
if (!inTransition()) {
// reset track-count now since shell-side is idle.
mTrackCount = 0;
}
updateRunningRemoteAnimation(record, false /* isPlaying */);
record.finishTransition();
for (int i = mAnimatingExitWindows.size() - 1; i >= 0; i--) {
final WindowState w = mAnimatingExitWindows.get(i);
if (w.mAnimatingExit && w.mHasSurface && !w.inTransition()) {
w.onExitAnimationDone();
}
if (!w.mAnimatingExit || !w.mHasSurface) {
mAnimatingExitWindows.remove(i);
}
}
mRunningLock.doNotifyLocked();
// Run state-validation checks when no transitions are active anymore (Note: sometimes
// finish can start a transition, so check afterwards -- eg. pip).
if (!inTransition()) {
validateStates();
mAtm.mWindowManager.onAnimationFinished();
}
}
/** Called by {@link Transition#finishTransition} if it committed invisible to any activities */
void onCommittedInvisibles() {
if (mCollectingTransition != null) {
mCollectingTransition.mPriorVisibilityMightBeDirty = true;
}
for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
mWaitingTransitions.get(i).mPriorVisibilityMightBeDirty = true;
}
}
private void validateStates() {
for (int i = 0; i < mStateValidators.size(); ++i) {
mStateValidators.get(i).run();
if (inTransition()) {
// the validator may have started a new transition, so wait for that before
// checking the rest.
mStateValidators.subList(0, i + 1).clear();
return;
}
}
mStateValidators.clear();
for (int i = 0; i < mValidateCommitVis.size(); ++i) {
final ActivityRecord ar = mValidateCommitVis.get(i);
if (!ar.isVisibleRequested() && ar.isVisible()) {
Slog.e(TAG, "Uncommitted visibility change: " + ar);
ar.commitVisibility(ar.isVisibleRequested(), false /* layout */,
false /* fromTransition */);
}
}
mValidateCommitVis.clear();
}
/**
* Called when the transition has a complete set of participants for its operation. In other
* words, it is when the transition is "ready" but is still waiting for participants to draw.
*/
void onTransitionPopulated(Transition transition) {
tryStartCollectFromQueue();
}
private boolean canStartCollectingNow(@Nullable Transition queued) {
if (mCollectingTransition == null) return true;
// Population (collect until ready) is still serialized, so always wait for that.
if (!mCollectingTransition.isPopulated()) return false;
// Check if queued *can* be independent with all collecting/waiting transitions.
if (!getCanBeIndependent(mCollectingTransition, queued)) return false;
for (int i = 0; i < mWaitingTransitions.size(); ++i) {
if (!getCanBeIndependent(mWaitingTransitions.get(i), queued)) return false;
}
return true;
}
void tryStartCollectFromQueue() {
if (mQueuedTransitions.isEmpty()) return;
// Only need to try the next one since, even when transition can collect in parallel,
// they still need to serialize on readiness.
final QueuedTransition queued = mQueuedTransitions.get(0);
if (mCollectingTransition != null) {
// If it's a legacy sync, then it needs to wait until there is no collecting transition.
if (queued.mTransition == null) return;
if (!canStartCollectingNow(queued.mTransition)) return;
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Moving #%d from collecting"
+ " to waiting.", mCollectingTransition.getSyncId());
mWaitingTransitions.add(mCollectingTransition);
mCollectingTransition = null;
} else if (mSyncEngine.hasActiveSync()) {
// A legacy transition is on-going, so we must wait.
return;
}
mQueuedTransitions.remove(0);
// This needs to happen immediately to prevent another sync from claiming the syncset
// out-of-order (moveToCollecting calls startSyncSet)
if (queued.mTransition != null) {
moveToCollecting(queued.mTransition);
} else {
// legacy sync
mSyncEngine.startSyncSet(queued.mLegacySync);
}
// Post this so that the now-playing transition logic isn't interrupted.
mAtm.mH.post(() -> {
synchronized (mAtm.mGlobalLock) {
queued.mOnStartCollect.onCollectStarted(true /* deferred */);
}
});
}
void moveToPlaying(Transition transition) {
if (transition == mCollectingTransition) {
mCollectingTransition = null;
if (!mWaitingTransitions.isEmpty()) {
mCollectingTransition = mWaitingTransitions.remove(0);
}
if (mCollectingTransition == null) {
// nothing collecting anymore, so clear order records.
mLatestOnTopTasksReported.clear();
}
} else {
if (!mWaitingTransitions.remove(transition)) {
throw new IllegalStateException("Trying to move non-collecting transition to"
+ "playing " + transition.getSyncId());
}
}
mPlayingTransitions.add(transition);
updateRunningRemoteAnimation(transition, true /* isPlaying */);
// Sync engine should become idle after this, so the idle listener will check the queue.
}
/**
* Checks if the `queued` transition has the potential to run independently of the
* `collecting` transition. It may still ultimately block in sync-engine or become dependent
* in {@link #getIsIndependent} later.
*/
boolean getCanBeIndependent(Transition collecting, @Nullable Transition queued) {
// For tests
if (queued != null && queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL
&& collecting.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) {
return true;
}
// For recents
if (queued != null && queued.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) {
if (collecting.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) {
// Must serialize with itself.
return false;
}
// allow this if `collecting` only has activities
for (int i = 0; i < collecting.mParticipants.size(); ++i) {
final WindowContainer wc = collecting.mParticipants.valueAt(i);
final ActivityRecord ar = wc.asActivityRecord();
if (ar == null && wc.asWindowState() == null && wc.asWindowToken() == null) {
// Is task or above, so can't be independent
return false;
}
if (ar != null && ar.isActivityTypeHomeOrRecents()) {
// It's a recents or home type, so it conflicts.
return false;
}
}
return true;
} else if (collecting.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) {
// We can collect simultaneously with recents if it is populated. This is because
// we know that recents will not collect/trampoline any more stuff. If anything in the
// queued transition overlaps, it will end up just waiting in sync-queue anyways.
return true;
}
return false;
}
/**
* Checks if `incoming` transition can run independently of `running` transition assuming that
* `running` is playing based on its current state.
*/
static boolean getIsIndependent(Transition running, Transition incoming) {
// For tests
if (running.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL
&& incoming.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) {
return true;
}
// For now there's only one mutually-independent pair: an all activity-level transition and
// a transient-launch where none of the activities are part of the transient-launch task,
// so the following logic is hard-coded specifically for this.
// Also, we currently restrict valid transient-launches to just recents.
final Transition recents;
final Transition other;
if (running.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS
&& running.hasTransientLaunch()) {
if (incoming.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS) {
// Recents can't be independent from itself.
return false;
}
recents = running;
other = incoming;
} else if (incoming.mParallelCollectType == Transition.PARALLEL_TYPE_RECENTS
&& incoming.hasTransientLaunch()) {
recents = incoming;
other = running;
} else {
return false;
}
// Check against *targets* because that is the post-promotion set of containers that are
// actually animating.
for (int i = 0; i < other.mTargets.size(); ++i) {
final WindowContainer wc = other.mTargets.get(i).mContainer;
final ActivityRecord ar = wc.asActivityRecord();
if (ar == null && wc.asWindowState() == null && wc.asWindowToken() == null) {
// Is task or above, so for now don't let them be independent.
return false;
}
if (ar != null && recents.isTransientLaunch(ar)) {
// Change overlaps with recents, so serialize.
return false;
}
}
return true;
}
void assignTrack(Transition transition, TransitionInfo info) {
int track = -1;
boolean sync = false;
for (int i = 0; i < mPlayingTransitions.size(); ++i) {
// ignore ourself obviously
if (mPlayingTransitions.get(i) == transition) continue;
if (getIsIndependent(mPlayingTransitions.get(i), transition)) continue;
if (track >= 0) {
// At this point, transition overlaps with multiple tracks, so just wait for
// everything
sync = true;
break;
}
track = mPlayingTransitions.get(i).mAnimationTrack;
}
if (sync) {
track = 0;
}
if (track < 0) {
// Didn't overlap with anything, so give it its own track
track = mTrackCount;
if (track > 0) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Playing #%d in parallel on "
+ "track #%d", transition.getSyncId(), track);
}
}
if (sync) {
info.setFlags(info.getFlags() | TransitionInfo.FLAG_SYNC);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Marking #%d animation as SYNC.",
transition.getSyncId());
}
transition.mAnimationTrack = track;
info.setTrack(track);
mTrackCount = Math.max(mTrackCount, track + 1);
}
void updateAnimatingState(SurfaceControl.Transaction t) {
final boolean animatingState = !mPlayingTransitions.isEmpty()
|| (mCollectingTransition != null && mCollectingTransition.isStarted());
if (animatingState && !mAnimatingState) {
t.setEarlyWakeupStart();
// Usually transitions put quite a load onto the system already (with all the things
// happening in app), so pause task snapshot persisting to not increase the load.
mSnapshotController.setPause(true);
mAnimatingState = true;
Transition.asyncTraceBegin("animating", 0x41bfaf1 /* hashcode of TAG */);
} else if (!animatingState && mAnimatingState) {
t.setEarlyWakeupEnd();
mSnapshotController.setPause(false);
mAnimatingState = false;
Transition.asyncTraceEnd(0x41bfaf1 /* hashcode of TAG */);
}
}
/** Updates the process state of animation player. */
private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) {
if (mTransitionPlayerProc == null) return;
if (isPlaying) {
mTransitionPlayerProc.setRunningRemoteAnimation(true);
} else if (mPlayingTransitions.isEmpty()) {
mTransitionPlayerProc.setRunningRemoteAnimation(false);
mRemotePlayer.clear();
return;
}
final IApplicationThread appThread = transition.getRemoteAnimationApp();
if (appThread == null || appThread == mTransitionPlayerProc.getThread()) return;
final WindowProcessController delegate = mAtm.getProcessController(appThread);
if (delegate == null) return;
mRemotePlayer.update(delegate, isPlaying, true /* predict */);
}
/** Called when a transition is aborted. This should only be called by {@link Transition} */
void onAbort(Transition transition) {
if (transition != mCollectingTransition) {
int waitingIdx = mWaitingTransitions.indexOf(transition);
if (waitingIdx < 0) {
throw new IllegalStateException("Too late for abort.");
}
mWaitingTransitions.remove(waitingIdx);
} else {
mCollectingTransition = null;
if (!mWaitingTransitions.isEmpty()) {
mCollectingTransition = mWaitingTransitions.remove(0);
}
if (mCollectingTransition == null) {
// nothing collecting anymore, so clear order records.
mLatestOnTopTasksReported.clear();
}
}
// This is called during Transition.abort whose codepath will eventually check the queue
// via sync-engine idle.
}
/**
* Record that the launch of {@param activity} is transient (meaning its lifecycle is currently
* tied to the transition).
* @param restoreBelowTask If non-null, the activity's task will be ordered right below this
* task if requested.
*/
void setTransientLaunch(@NonNull ActivityRecord activity, @Nullable Task restoreBelowTask) {
if (mCollectingTransition == null) return;
mCollectingTransition.setTransientLaunch(activity, restoreBelowTask);
// TODO(b/188669821): Remove once legacy recents behavior is moved to shell.
// Also interpret HOME transient launch as recents
if (activity.isActivityTypeHomeOrRecents()) {
mCollectingTransition.addFlag(TRANSIT_FLAG_IS_RECENTS);
// When starting recents animation, we assume the recents activity is behind the app
// task and should not affect system bar appearance,
// until WMS#setRecentsAppBehindSystemBars be called from launcher when passing
// the gesture threshold.
activity.getTask().setCanAffectSystemUiFlags(false);
}
}
/** @see Transition#setCanPipOnFinish */
void setCanPipOnFinish(boolean canPipOnFinish) {
if (mCollectingTransition == null) return;
mCollectingTransition.setCanPipOnFinish(canPipOnFinish);
}
void legacyDetachNavigationBarFromApp(@NonNull IBinder token) {
final Transition transition = Transition.fromBinder(token);
if (transition == null || !mPlayingTransitions.contains(transition)) {
Slog.e(TAG, "Transition isn't playing: " + token);
return;
}
transition.legacyRestoreNavigationBarFromApp();
}
void registerLegacyListener(WindowManagerInternal.AppTransitionListener listener) {
mLegacyListeners.add(listener);
}
void unregisterLegacyListener(WindowManagerInternal.AppTransitionListener listener) {
mLegacyListeners.remove(listener);
}
void dispatchLegacyAppTransitionPending() {
for (int i = 0; i < mLegacyListeners.size(); ++i) {
mLegacyListeners.get(i).onAppTransitionPendingLocked();
}
}
void dispatchLegacyAppTransitionStarting(TransitionInfo info, long statusBarTransitionDelay) {
for (int i = 0; i < mLegacyListeners.size(); ++i) {
// TODO(shell-transitions): handle (un)occlude transition.
mLegacyListeners.get(i).onAppTransitionStartingLocked(
SystemClock.uptimeMillis() + statusBarTransitionDelay,
AnimationAdapter.STATUS_BAR_TRANSITION_DURATION);
}
}
void dispatchLegacyAppTransitionFinished(ActivityRecord ar) {
for (int i = 0; i < mLegacyListeners.size(); ++i) {
mLegacyListeners.get(i).onAppTransitionFinishedLocked(ar.token);
}
}
void dispatchLegacyAppTransitionCancelled() {
for (int i = 0; i < mLegacyListeners.size(); ++i) {
mLegacyListeners.get(i).onAppTransitionCancelledLocked(
false /* keyguardGoingAwayCancelled */);
}
}
void dumpDebugLegacy(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
int state = LEGACY_STATE_IDLE;
if (!mPlayingTransitions.isEmpty()) {
state = LEGACY_STATE_RUNNING;
} else if ((mCollectingTransition != null && mCollectingTransition.getLegacyIsReady())
|| mSyncEngine.hasPendingSyncSets()) {
// The transition may not be "ready", but we have a sync-transaction waiting to start.
// Usually the pending transaction is for a transition, so assuming that is the case,
// we can't be IDLE for test purposes. Ideally, we should have a STATE_COLLECTING.
state = LEGACY_STATE_READY;
}
proto.write(AppTransitionProto.APP_TRANSITION_STATE, state);
proto.end(token);
}
/** Returns {@code true} if it started collecting, {@code false} if it was queued. */
private void queueTransition(Transition transit, OnStartCollect onStartCollect) {
mQueuedTransitions.add(new QueuedTransition(transit, onStartCollect));
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN,
"Queueing transition: %s", transit);
}
/** Returns {@code true} if it started collecting, {@code false} if it was queued. */
boolean startCollectOrQueue(Transition transit, OnStartCollect onStartCollect) {
if (!mQueuedTransitions.isEmpty()) {
// Just add to queue since we already have a queue.
queueTransition(transit, onStartCollect);
return false;
}
if (mSyncEngine.hasActiveSync()) {
if (isCollecting()) {
// Check if we can run in parallel here.
if (canStartCollectingNow(transit)) {
// start running in parallel.
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Moving #%d from"
+ " collecting to waiting.", mCollectingTransition.getSyncId());
mWaitingTransitions.add(mCollectingTransition);
mCollectingTransition = null;
moveToCollecting(transit);
onStartCollect.onCollectStarted(false /* deferred */);
return true;
}
} else {
Slog.w(TAG, "Ongoing Sync outside of transition.");
}
queueTransition(transit, onStartCollect);
return false;
}
moveToCollecting(transit);
onStartCollect.onCollectStarted(false /* deferred */);
return true;
}
/**
* This will create and start collecting for a transition if possible. If there's no way to
* start collecting for `parallelType` now, then this returns null.
*
* WARNING: ONLY use this if the transition absolutely cannot be deferred!
*/
@NonNull
Transition createAndStartCollecting(int type) {
if (mTransitionPlayer == null) {
return null;
}
if (!mQueuedTransitions.isEmpty()) {
// There is a queue, so it's not possible to start immediately
return null;
}
if (mSyncEngine.hasActiveSync()) {
if (isCollecting()) {
// Check if we can run in parallel here.
if (canStartCollectingNow(null /* transit */)) {
// create and collect in parallel.
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Moving #%d from"
+ " collecting to waiting.", mCollectingTransition.getSyncId());
mWaitingTransitions.add(mCollectingTransition);
mCollectingTransition = null;
Transition transit = new Transition(type, 0 /* flags */, this, mSyncEngine);
moveToCollecting(transit);
return transit;
}
} else {
Slog.w(TAG, "Ongoing Sync outside of transition.");
}
return null;
}
Transition transit = new Transition(type, 0 /* flags */, this, mSyncEngine);
moveToCollecting(transit);
return transit;
}
/** Returns {@code true} if it started collecting, {@code false} if it was queued. */
boolean startLegacySyncOrQueue(BLASTSyncEngine.SyncGroup syncGroup, Runnable applySync) {
if (!mQueuedTransitions.isEmpty() || mSyncEngine.hasActiveSync()) {
// Just add to queue since we already have a queue.
mQueuedTransitions.add(new QueuedTransition(syncGroup, (d) -> applySync.run()));
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN,
"Queueing legacy sync-set: %s", syncGroup.mSyncId);
return false;
}
mSyncEngine.startSyncSet(syncGroup);
applySync.run();
return true;
}
interface OnStartCollect {
void onCollectStarted(boolean deferred);
}
/**
* This manages the animating state of processes that are running remote animations for
* {@link #mTransitionPlayerProc}.
*/
static class RemotePlayer {
private static final long REPORT_RUNNING_GRACE_PERIOD_MS = 100;
@GuardedBy("itself")
private final ArrayMap<IBinder, DelegateProcess> mDelegateProcesses = new ArrayMap<>();
private final ActivityTaskManagerService mAtm;
private class DelegateProcess implements Runnable {
final WindowProcessController mProc;
/** Requires {@link RemotePlayer#reportRunning} to confirm it is really running. */
boolean mNeedReport;
DelegateProcess(WindowProcessController proc) {
mProc = proc;
}
/** This runs when the remote player doesn't report running in time. */
@Override
public void run() {
synchronized (mAtm.mGlobalLockWithoutBoost) {
update(mProc, false /* running */, false /* predict */);
}
}
}
RemotePlayer(ActivityTaskManagerService atm) {
mAtm = atm;
}
void update(@NonNull WindowProcessController delegate, boolean running, boolean predict) {
if (!running) {
synchronized (mDelegateProcesses) {
boolean removed = false;
for (int i = mDelegateProcesses.size() - 1; i >= 0; i--) {
if (mDelegateProcesses.valueAt(i).mProc == delegate) {
mDelegateProcesses.removeAt(i);
removed = true;
break;
}
}
if (!removed) return;
}
delegate.setRunningRemoteAnimation(false);
return;
}
if (delegate.isRunningRemoteTransition() || !delegate.hasThread()) return;
delegate.setRunningRemoteAnimation(true);
final DelegateProcess delegateProc = new DelegateProcess(delegate);
// If "predict" is true, that means the remote animation is set from
// ActivityOptions#makeRemoteAnimation(). But it is still up to shell side to decide
// whether to use the remote animation, so there is a timeout to cancel the prediction
// if the remote animation doesn't happen.
if (predict) {
delegateProc.mNeedReport = true;
mAtm.mH.postDelayed(delegateProc, REPORT_RUNNING_GRACE_PERIOD_MS);
}
synchronized (mDelegateProcesses) {
mDelegateProcesses.put(delegate.getThread().asBinder(), delegateProc);
}
}
void clear() {
synchronized (mDelegateProcesses) {
for (int i = mDelegateProcesses.size() - 1; i >= 0; i--) {
mDelegateProcesses.valueAt(i).mProc.setRunningRemoteAnimation(false);
}
mDelegateProcesses.clear();
}
}
/** Returns {@code true} if the app is known to be running remote transition. */
boolean reportRunning(@NonNull IApplicationThread appThread) {
final DelegateProcess delegate;
synchronized (mDelegateProcesses) {
delegate = mDelegateProcesses.get(appThread.asBinder());
if (delegate != null && delegate.mNeedReport) {
// It was predicted to run remote transition. Now it is really requesting so
// remove the timeout of restoration.
delegate.mNeedReport = false;
mAtm.mH.removeCallbacks(delegate);
}
}
return delegate != null;
}
}
/**
* Data-class to store recorded events/info for a transition. This allows us to defer the
* actual logging until the system isn't busy. This also records some common metrics to see
* delays at-a-glance.
*
* Beside `mCreateWallTimeMs`, all times are elapsed times and will all be reported relative
* to when the transition was created.
*/
static class Logger {
long mCreateWallTimeMs;
long mCreateTimeNs;
long mRequestTimeNs;
long mCollectTimeNs;
long mStartTimeNs;
long mReadyTimeNs;
long mSendTimeNs;
long mFinishTimeNs;
long mAbortTimeNs;
TransitionRequestInfo mRequest;
WindowContainerTransaction mStartWCT;
int mSyncId;
TransitionInfo mInfo;
private String buildOnSendLog() {
StringBuilder sb = new StringBuilder("Sent Transition #").append(mSyncId)
.append(" createdAt=").append(TimeUtils.logTimeOfDay(mCreateWallTimeMs));
if (mRequest != null) {
sb.append(" via request=").append(mRequest);
}
return sb.toString();
}
void logOnSend() {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "%s", buildOnSendLog());
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, " startWCT=%s", mStartWCT);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, " info=%s", mInfo);
}
private static String toMsString(long nanos) {
return ((double) Math.round((double) nanos / 1000) / 1000) + "ms";
}
private String buildOnFinishLog() {
StringBuilder sb = new StringBuilder("Finish Transition #").append(mSyncId)
.append(": created at ").append(TimeUtils.logTimeOfDay(mCreateWallTimeMs));
sb.append(" collect-started=").append(toMsString(mCollectTimeNs - mCreateTimeNs));
if (mRequestTimeNs != 0) {
sb.append(" request-sent=").append(toMsString(mRequestTimeNs - mCreateTimeNs));
}
sb.append(" started=").append(toMsString(mStartTimeNs - mCreateTimeNs));
sb.append(" ready=").append(toMsString(mReadyTimeNs - mCreateTimeNs));
sb.append(" sent=").append(toMsString(mSendTimeNs - mCreateTimeNs));
sb.append(" finished=").append(toMsString(mFinishTimeNs - mCreateTimeNs));
return sb.toString();
}
void logOnFinish() {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "%s", buildOnFinishLog());
}
}
static class TransitionMetricsReporter extends ITransitionMetricsReporter.Stub {
private final ArrayMap<IBinder, LongConsumer> mMetricConsumers = new ArrayMap<>();
void associate(IBinder transitionToken, LongConsumer consumer) {
synchronized (mMetricConsumers) {
mMetricConsumers.put(transitionToken, consumer);
}
}
@Override
public void reportAnimationStart(IBinder transitionToken, long startTime) {
final LongConsumer c;
synchronized (mMetricConsumers) {
if (mMetricConsumers.isEmpty()) return;
c = mMetricConsumers.remove(transitionToken);
}
if (c != null) {
c.accept(startTime);
}
}
}
class Lock {
private int mTransitionWaiters = 0;
void runWhenIdle(long timeout, Runnable r) {
synchronized (mAtm.mGlobalLock) {
if (!inTransition()) {
r.run();
return;
}
mTransitionWaiters += 1;
}
final long startTime = SystemClock.uptimeMillis();
final long endTime = startTime + timeout;
while (true) {
synchronized (mAtm.mGlobalLock) {
if (!inTransition() || SystemClock.uptimeMillis() > endTime) {
mTransitionWaiters -= 1;
r.run();
return;
}
}
synchronized (this) {
try {
this.wait(timeout);
} catch (InterruptedException e) {
return;
}
}
}
}
void doNotifyLocked() {
synchronized (this) {
if (mTransitionWaiters > 0) {
this.notifyAll();
}
}
}
}
}