Merge "Decompose prepareAnimationIfNeeded into smaller methods."
This commit is contained in:
committed by
Android (Google) Code Review
commit
49079f3b9d
@@ -19,7 +19,6 @@ package com.android.server.wm;
|
||||
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
||||
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
|
||||
import static android.view.RemoteAnimationTarget.MODE_OPENING;
|
||||
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
|
||||
import static android.view.WindowManager.TRANSIT_CLOSE;
|
||||
import static android.view.WindowManager.TRANSIT_TO_BACK;
|
||||
@@ -28,12 +27,12 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
|
||||
import static com.android.server.wm.BackNavigationProto.ANIMATION_IN_PROGRESS;
|
||||
import static com.android.server.wm.BackNavigationProto.LAST_BACK_TYPE;
|
||||
import static com.android.server.wm.BackNavigationProto.SHOW_WALLPAPER;
|
||||
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.HardwareBuffer;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteCallback;
|
||||
@@ -45,6 +44,7 @@ import android.util.proto.ProtoOutputStream;
|
||||
import android.view.IWindowFocusObserver;
|
||||
import android.view.RemoteAnimationTarget;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.WindowInsets;
|
||||
import android.window.BackAnimationAdapter;
|
||||
import android.window.BackNavigationInfo;
|
||||
import android.window.IBackAnimationFinishedCallback;
|
||||
@@ -54,8 +54,11 @@ import android.window.TaskSnapshot;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.wm.utils.InsetUtils;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Controller to handle actions related to the back gesture on the server side.
|
||||
@@ -69,7 +72,7 @@ class BackNavigationController {
|
||||
private boolean mShowWallpaper;
|
||||
private Runnable mPendingAnimation;
|
||||
|
||||
private final AnimationTargets mAnimationTargets = new AnimationTargets();
|
||||
private AnimationHandler mAnimationHandler;
|
||||
private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>();
|
||||
private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>();
|
||||
|
||||
@@ -233,7 +236,6 @@ class BackNavigationController {
|
||||
return infoBuilder.build();
|
||||
}
|
||||
|
||||
mBackAnimationInProgress = true;
|
||||
// We don't have an application callback, let's find the destination of the back gesture
|
||||
// The search logic should align with ActivityClientController#finishActivity
|
||||
prevActivity = currentTask.topRunningActivity(currentActivity.token, INVALID_TASK_ID);
|
||||
@@ -314,8 +316,17 @@ class BackNavigationController {
|
||||
}
|
||||
|
||||
if (prepareAnimation) {
|
||||
prepareAnimationIfNeeded(currentTask, prevTask, prevActivity,
|
||||
removedWindowContainer, backType, adapter);
|
||||
mPendingAnimation = mAnimationHandler.scheduleAnimation(backType, adapter,
|
||||
currentTask, prevTask, currentActivity, prevActivity);
|
||||
prepareAnimation = mPendingAnimation != null;
|
||||
mBackAnimationInProgress = prepareAnimation;
|
||||
if (prepareAnimation) {
|
||||
mWindowManagerService.mWindowPlacerLocked.requestTraversal();
|
||||
if (mShowWallpaper) {
|
||||
currentTask.getDisplayContent().mWallpaperController
|
||||
.adjustWallpaperWindows();
|
||||
}
|
||||
}
|
||||
}
|
||||
infoBuilder.setPrepareRemoteAnimation(prepareAnimation);
|
||||
} // Release wm Lock
|
||||
@@ -333,7 +344,7 @@ class BackNavigationController {
|
||||
}
|
||||
|
||||
boolean isWaitBackTransition() {
|
||||
return mAnimationTargets.mComposed && mAnimationTargets.mWaitTransition;
|
||||
return mAnimationHandler.mComposed && mAnimationHandler.mWaitTransition;
|
||||
}
|
||||
|
||||
boolean isKeyguardOccluded(WindowState focusWindow) {
|
||||
@@ -359,18 +370,19 @@ class BackNavigationController {
|
||||
boolean result = false;
|
||||
// Note: TmpOpenApps is empty. Unlike shell transition, the open apps will be removed from
|
||||
// mOpeningApps if there is no visibility change.
|
||||
if (mAnimationTargets.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps)) {
|
||||
if (mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps)) {
|
||||
// remove close target from close list, open target from open list;
|
||||
// but the open target can be in close list.
|
||||
for (int i = openApps.size() - 1; i >= 0; --i) {
|
||||
final ActivityRecord ar = openApps.valueAt(i);
|
||||
if (mAnimationTargets.isTarget(ar, true /* open */)) {
|
||||
if (mAnimationHandler.isTarget(ar, true /* open */)) {
|
||||
openApps.removeAt(i);
|
||||
mAnimationHandler.mOpenTransitionTargetMatch = true;
|
||||
}
|
||||
}
|
||||
for (int i = closeApps.size() - 1; i >= 0; --i) {
|
||||
final ActivityRecord ar = closeApps.valueAt(i);
|
||||
if (mAnimationTargets.isTarget(ar, false /* open */)) {
|
||||
if (mAnimationHandler.isTarget(ar, false /* open */)) {
|
||||
closeApps.removeAt(i);
|
||||
}
|
||||
}
|
||||
@@ -391,7 +403,7 @@ class BackNavigationController {
|
||||
* animations, and shouldn't join next transition.
|
||||
*/
|
||||
boolean containsBackAnimationTargets(Transition transition) {
|
||||
if (!mAnimationTargets.mComposed
|
||||
if (!mAnimationHandler.mComposed
|
||||
|| (transition.mType != TRANSIT_CLOSE && transition.mType != TRANSIT_TO_BACK)) {
|
||||
return false;
|
||||
}
|
||||
@@ -408,18 +420,22 @@ class BackNavigationController {
|
||||
mTmpCloseApps.add(wc);
|
||||
}
|
||||
}
|
||||
final boolean result = mAnimationTargets.containsBackAnimationTargets(
|
||||
final boolean result = mAnimationHandler.containsBackAnimationTargets(
|
||||
mTmpOpenApps, mTmpCloseApps);
|
||||
if (result) {
|
||||
mAnimationHandler.mOpenTransitionTargetMatch =
|
||||
mAnimationHandler.containTarget(mTmpOpenApps, true);
|
||||
}
|
||||
mTmpOpenApps.clear();
|
||||
mTmpCloseApps.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean isMonitorTransitionTarget(WindowContainer wc) {
|
||||
if (!mAnimationTargets.mComposed || !mAnimationTargets.mWaitTransition) {
|
||||
if (!mAnimationHandler.mComposed || !mAnimationHandler.mWaitTransition) {
|
||||
return false;
|
||||
}
|
||||
return mAnimationTargets.isTarget(wc, wc.isVisibleRequested() /* open */);
|
||||
return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -428,60 +444,106 @@ class BackNavigationController {
|
||||
* cleanup together.
|
||||
*/
|
||||
void clearBackAnimations(SurfaceControl.Transaction cleanupTransaction) {
|
||||
mAnimationTargets.clearBackAnimateTarget(cleanupTransaction);
|
||||
mAnimationHandler.clearBackAnimateTarget(cleanupTransaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Animation composer
|
||||
* prepareAnimationIfNeeded will become too complicated in order to support
|
||||
* ActivityRecord/WindowState, using a factory class to create the RemoteAnimationTargets for
|
||||
* different scenario.
|
||||
* Create and handling animations status for an open/close animation targets.
|
||||
*/
|
||||
private static class AnimationTargets {
|
||||
ActivityRecord mCloseTarget; // Must be activity
|
||||
WindowContainer mOpenTarget; // Can be activity or task if activity was removed
|
||||
private static class AnimationHandler {
|
||||
private final WindowManagerService mWindowManagerService;
|
||||
private BackWindowAnimationAdaptor mCloseAdaptor;
|
||||
private BackWindowAnimationAdaptor mOpenAdaptor;
|
||||
private boolean mComposed;
|
||||
private boolean mWaitTransition;
|
||||
private int mSwitchType = UNKNOWN;
|
||||
private SurfaceControl.Transaction mFinishedTransaction;
|
||||
// This will be set before transition happen, to know whether the real opening target
|
||||
// exactly match animating target. When target match, reparent the starting surface to
|
||||
// the opening target like starting window do.
|
||||
private boolean mOpenTransitionTargetMatch;
|
||||
// The starting surface task Id. Used to clear the starting surface if the animation has
|
||||
// request one during animating.
|
||||
private int mRequestedStartingSurfaceTaskId;
|
||||
private SurfaceControl mStartingSurface;
|
||||
|
||||
AnimationHandler(WindowManagerService wms) {
|
||||
mWindowManagerService = wms;
|
||||
}
|
||||
private static final int UNKNOWN = 0;
|
||||
private static final int TASK_SWITCH = 1;
|
||||
private static final int ACTIVITY_SWITCH = 2;
|
||||
|
||||
void reset(@NonNull WindowContainer close, @NonNull WindowContainer open) {
|
||||
clearBackAnimateTarget(null);
|
||||
if (close == null || open == null) {
|
||||
Slog.e(TAG, "reset animation with null target close: "
|
||||
+ close + " open: " + open);
|
||||
return;
|
||||
}
|
||||
private void initiate(WindowContainer close, WindowContainer open) {
|
||||
WindowContainer closeTarget;
|
||||
if (close.asActivityRecord() != null && open.asActivityRecord() != null
|
||||
&& (close.asActivityRecord().getTask() == open.asActivityRecord().getTask())) {
|
||||
mSwitchType = ACTIVITY_SWITCH;
|
||||
mCloseTarget = close.asActivityRecord();
|
||||
closeTarget = close.asActivityRecord();
|
||||
} else if (close.asTask() != null && open.asTask() != null
|
||||
&& close.asTask() != open.asTask()) {
|
||||
mSwitchType = TASK_SWITCH;
|
||||
mCloseTarget = close.asTask().getTopNonFinishingActivity();
|
||||
closeTarget = close.asTask().getTopNonFinishingActivity();
|
||||
} else {
|
||||
mSwitchType = UNKNOWN;
|
||||
return;
|
||||
}
|
||||
|
||||
mOpenTarget = open;
|
||||
mComposed = false;
|
||||
mWaitTransition = false;
|
||||
mCloseAdaptor = createAdaptor(closeTarget, false /* isOpen */);
|
||||
mOpenAdaptor = createAdaptor(open, true /* isOpen */);
|
||||
|
||||
if (mCloseAdaptor.mAnimationTarget == null || mOpenAdaptor.mAnimationTarget == null) {
|
||||
Slog.w(TAG, "composeNewAnimations fail, skip");
|
||||
clearBackAnimateTarget(null /* cleanupTransaction */);
|
||||
}
|
||||
}
|
||||
|
||||
void composeNewAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) {
|
||||
reset(close, open);
|
||||
if (mSwitchType == UNKNOWN || mComposed || mCloseTarget == mOpenTarget
|
||||
|| mCloseTarget == null || mOpenTarget == null) {
|
||||
return;
|
||||
boolean composeAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) {
|
||||
clearBackAnimateTarget(null /* cleanupTransaction */);
|
||||
if (close == null || open == null) {
|
||||
Slog.e(TAG, "reset animation with null target close: "
|
||||
+ close + " open: " + open);
|
||||
return false;
|
||||
}
|
||||
initiate(close, open);
|
||||
if (mSwitchType == UNKNOWN) {
|
||||
return false;
|
||||
}
|
||||
mComposed = true;
|
||||
mWaitTransition = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
RemoteAnimationTarget[] getAnimationTargets() {
|
||||
return mComposed ? new RemoteAnimationTarget[] {
|
||||
mCloseAdaptor.mAnimationTarget, mOpenAdaptor.mAnimationTarget} : null;
|
||||
}
|
||||
|
||||
boolean isSupportWindowlessSurface() {
|
||||
return mWindowManagerService.mAtmService.mTaskOrganizerController
|
||||
.isSupportWindowlessStartingSurface();
|
||||
}
|
||||
|
||||
void createStartingSurface(TaskSnapshot snapshot) {
|
||||
if (!mComposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ActivityRecord topActivity = getTopOpenActivity();
|
||||
if (topActivity == null) {
|
||||
Slog.e(TAG, "createStartingSurface fail, no open activity: " + this);
|
||||
return;
|
||||
}
|
||||
// TODO (b/257857570) draw snapshot by starting surface.
|
||||
}
|
||||
|
||||
private ActivityRecord getTopOpenActivity() {
|
||||
if (mSwitchType == ACTIVITY_SWITCH) {
|
||||
return mOpenAdaptor.mTarget.asActivityRecord();
|
||||
} else if (mSwitchType == TASK_SWITCH) {
|
||||
return mOpenAdaptor.mTarget.asTask().getTopNonFinishingActivity();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean containTarget(ArrayList<WindowContainer> wcs, boolean open) {
|
||||
@@ -495,13 +557,13 @@ class BackNavigationController {
|
||||
|
||||
boolean isTarget(WindowContainer wc, boolean open) {
|
||||
if (open) {
|
||||
return wc == mOpenTarget || mOpenTarget.hasChild(wc);
|
||||
return wc == mOpenAdaptor.mTarget || mOpenAdaptor.mTarget.hasChild(wc);
|
||||
}
|
||||
if (mSwitchType == TASK_SWITCH) {
|
||||
return wc == mCloseTarget
|
||||
|| (wc.asTask() != null && wc.hasChild(mCloseTarget));
|
||||
return wc == mCloseAdaptor.mTarget
|
||||
|| (wc.asTask() != null && wc.hasChild(mCloseAdaptor.mTarget));
|
||||
} else if (mSwitchType == ACTIVITY_SWITCH) {
|
||||
return wc == mCloseTarget;
|
||||
return wc == mCloseAdaptor.mTarget;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -519,19 +581,53 @@ class BackNavigationController {
|
||||
return;
|
||||
}
|
||||
final SurfaceControl.Transaction pt = t != null ? t
|
||||
: mOpenTarget.getPendingTransaction();
|
||||
: mOpenAdaptor.mTarget.getPendingTransaction();
|
||||
if (mFinishedTransaction != null) {
|
||||
pt.merge(mFinishedTransaction);
|
||||
mFinishedTransaction = null;
|
||||
}
|
||||
cleanUpWindowlessSurface();
|
||||
|
||||
if (mCloseAdaptor != null) {
|
||||
mCloseAdaptor.mTarget.cancelAnimation();
|
||||
mCloseAdaptor = null;
|
||||
}
|
||||
if (mOpenAdaptor != null) {
|
||||
mOpenAdaptor.mTarget.cancelAnimation();
|
||||
mOpenAdaptor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanUpWindowlessSurface() {
|
||||
final ActivityRecord ar = getTopOpenActivity();
|
||||
if (ar == null) {
|
||||
Slog.w(TAG, "finishPresentAnimations without top activity: " + this);
|
||||
}
|
||||
final SurfaceControl.Transaction pendingT = ar != null ? ar.getPendingTransaction()
|
||||
: mOpenAdaptor.mTarget.getPendingTransaction();
|
||||
// ensure open target is visible before cancel animation.
|
||||
mOpenTransitionTargetMatch &= ar != null;
|
||||
if (mOpenTransitionTargetMatch) {
|
||||
pendingT.show(ar.getSurfaceControl());
|
||||
}
|
||||
if (mRequestedStartingSurfaceTaskId != 0) {
|
||||
// If open target match, reparent to open activity
|
||||
if (mStartingSurface != null && mOpenTransitionTargetMatch) {
|
||||
pendingT.reparent(mStartingSurface, ar.getSurfaceControl());
|
||||
}
|
||||
// remove starting surface.
|
||||
mStartingSurface = null;
|
||||
// TODO (b/257857570) draw snapshot by starting surface.
|
||||
mRequestedStartingSurfaceTaskId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void clearBackAnimateTarget(SurfaceControl.Transaction cleanupTransaction) {
|
||||
finishPresentAnimations(cleanupTransaction);
|
||||
mCloseTarget = null;
|
||||
mOpenTarget = null;
|
||||
mComposed = false;
|
||||
mWaitTransition = false;
|
||||
mOpenTransitionTargetMatch = false;
|
||||
mRequestedStartingSurfaceTaskId = 0;
|
||||
mSwitchType = UNKNOWN;
|
||||
if (mFinishedTransaction != null) {
|
||||
Slog.w(TAG, "Clear back animation, found un-processed finished transaction");
|
||||
@@ -555,211 +651,284 @@ class BackNavigationController {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder(128);
|
||||
sb.append("AnimationTargets{");
|
||||
sb.append(" mOpenTarget= ");
|
||||
sb.append(mOpenTarget);
|
||||
sb.append(" mCloseTarget= ");
|
||||
sb.append(mCloseTarget);
|
||||
sb.append(" mSwitchType= ");
|
||||
sb.append(mSwitchType);
|
||||
sb.append(" mComposed= ");
|
||||
sb.append(mComposed);
|
||||
sb.append(" mWaitTransition= ");
|
||||
sb.append(mWaitTransition);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
return "AnimationTargets{"
|
||||
+ " openTarget= "
|
||||
+ mOpenAdaptor.mTarget
|
||||
+ " closeTarget= "
|
||||
+ mCloseAdaptor.mTarget
|
||||
+ " mSwitchType= "
|
||||
+ mSwitchType
|
||||
+ " mComposed= "
|
||||
+ mComposed
|
||||
+ " mWaitTransition= "
|
||||
+ mWaitTransition
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareAnimationIfNeeded(Task currentTask,
|
||||
Task prevTask, ActivityRecord prevActivity, WindowContainer<?> removedWindowContainer,
|
||||
int backType, BackAnimationAdapter adapter) {
|
||||
final ArrayList<SurfaceControl> leashes = new ArrayList<>();
|
||||
final SurfaceControl.Transaction startedTransaction = currentTask.getPendingTransaction();
|
||||
final SurfaceControl.Transaction finishedTransaction = new SurfaceControl.Transaction();
|
||||
// Prepare a leash to animate for the departing window
|
||||
final SurfaceControl animLeash = currentTask.makeAnimationLeash()
|
||||
.setName("BackPreview Leash for " + currentTask)
|
||||
.setHidden(false)
|
||||
.build();
|
||||
removedWindowContainer.reparentSurfaceControl(startedTransaction, animLeash);
|
||||
private static BackWindowAnimationAdaptor createAdaptor(
|
||||
WindowContainer target, boolean isOpen) {
|
||||
final BackWindowAnimationAdaptor adaptor =
|
||||
new BackWindowAnimationAdaptor(target, isOpen);
|
||||
target.startAnimation(target.getPendingTransaction(), adaptor, false /* hidden */,
|
||||
ANIMATION_TYPE_PREDICT_BACK);
|
||||
return adaptor;
|
||||
}
|
||||
|
||||
final RemoteAnimationTarget topAppTarget = createRemoteAnimationTargetLocked(
|
||||
currentTask, animLeash, MODE_CLOSING);
|
||||
private static class BackWindowAnimationAdaptor implements AnimationAdapter {
|
||||
SurfaceControl mCapturedLeash;
|
||||
private final Rect mBounds = new Rect();
|
||||
private final WindowContainer mTarget;
|
||||
private final boolean mIsOpen;
|
||||
private RemoteAnimationTarget mAnimationTarget;
|
||||
|
||||
// reset leash after animation finished.
|
||||
leashes.add(animLeash);
|
||||
removedWindowContainer.reparentSurfaceControl(finishedTransaction,
|
||||
removedWindowContainer.getParentSurfaceControl());
|
||||
BackWindowAnimationAdaptor(WindowContainer closeTarget, boolean isOpen) {
|
||||
mBounds.set(closeTarget.getBounds());
|
||||
mTarget = closeTarget;
|
||||
mIsOpen = isOpen;
|
||||
}
|
||||
@Override
|
||||
public boolean getShowWallpaper() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare a leash to animate for the entering window.
|
||||
RemoteAnimationTarget behindAppTarget = null;
|
||||
if (needsScreenshot(backType)) {
|
||||
HardwareBuffer screenshotBuffer = null;
|
||||
Task backTargetTask = prevTask;
|
||||
switch(backType) {
|
||||
case BackNavigationInfo.TYPE_CROSS_TASK:
|
||||
int prevTaskId = prevTask != null ? prevTask.mTaskId : 0;
|
||||
int prevUserId = prevTask != null ? prevTask.mUserId : 0;
|
||||
screenshotBuffer = getTaskSnapshot(prevTaskId, prevUserId);
|
||||
break;
|
||||
@Override
|
||||
public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
|
||||
int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
|
||||
mCapturedLeash = animationLeash;
|
||||
createRemoteAnimationTarget(mIsOpen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancelled(SurfaceControl animationLeash) {
|
||||
if (mCapturedLeash == animationLeash) {
|
||||
mCapturedLeash = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationHint() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStatusBarTransitionsStartTime() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(PrintWriter pw, String prefix) {
|
||||
pw.print(prefix + "BackWindowAnimationAdaptor mCapturedLeash=");
|
||||
pw.print(mCapturedLeash);
|
||||
pw.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpDebug(ProtoOutputStream proto) {
|
||||
|
||||
}
|
||||
|
||||
RemoteAnimationTarget createRemoteAnimationTarget(boolean isOpen) {
|
||||
if (mAnimationTarget != null) {
|
||||
return mAnimationTarget;
|
||||
}
|
||||
Task t = mTarget.asTask();
|
||||
final ActivityRecord r = t != null ? t.getTopNonFinishingActivity()
|
||||
: mTarget.asActivityRecord();
|
||||
if (t == null && r != null) {
|
||||
t = r.getTask();
|
||||
}
|
||||
if (t == null || r == null) {
|
||||
Slog.e(TAG, "createRemoteAnimationTarget fail " + mTarget);
|
||||
return null;
|
||||
}
|
||||
final WindowState mainWindow = r.findMainWindow();
|
||||
Rect insets;
|
||||
if (mainWindow != null) {
|
||||
insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets(
|
||||
mBounds, WindowInsets.Type.systemBars(),
|
||||
false /* ignoreVisibility */).toRect();
|
||||
InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets());
|
||||
} else {
|
||||
insets = new Rect();
|
||||
}
|
||||
final int mode = isOpen ? MODE_OPENING : MODE_CLOSING;
|
||||
mAnimationTarget = new RemoteAnimationTarget(t.mTaskId, mode, mCapturedLeash,
|
||||
!r.fillsParent(), new Rect(),
|
||||
insets, r.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top),
|
||||
mBounds, mBounds, t.getWindowConfiguration(),
|
||||
true /* isNotInRecents */, null, null, t.getTaskInfo(),
|
||||
r.checkEnterPictureInPictureAppOpsState());
|
||||
return mAnimationTarget;
|
||||
}
|
||||
}
|
||||
|
||||
Runnable scheduleAnimation(int backType, BackAnimationAdapter adapter,
|
||||
Task currentTask, Task previousTask, ActivityRecord currentActivity,
|
||||
ActivityRecord previousActivity) {
|
||||
switch (backType) {
|
||||
case BackNavigationInfo.TYPE_RETURN_TO_HOME:
|
||||
return new ScheduleAnimationBuilder(backType, adapter)
|
||||
.setIsLaunchBehind(true)
|
||||
.setComposeTarget(currentTask, previousTask)
|
||||
.build();
|
||||
case BackNavigationInfo.TYPE_CROSS_ACTIVITY:
|
||||
if (prevActivity != null && prevActivity.mActivityComponent != null) {
|
||||
screenshotBuffer = getActivitySnapshot(currentTask, prevActivity);
|
||||
return new ScheduleAnimationBuilder(backType, adapter)
|
||||
.setComposeTarget(currentActivity, previousActivity)
|
||||
.setOpeningSnapshot(getActivitySnapshot(previousActivity)).build();
|
||||
case BackNavigationInfo.TYPE_CROSS_TASK:
|
||||
return new ScheduleAnimationBuilder(backType, adapter)
|
||||
.setComposeTarget(currentTask, previousTask)
|
||||
.setOpeningSnapshot(getTaskSnapshot(previousTask)).build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private class ScheduleAnimationBuilder {
|
||||
final int mType;
|
||||
final BackAnimationAdapter mBackAnimationAdapter;
|
||||
WindowContainer mCloseTarget;
|
||||
WindowContainer mOpenTarget;
|
||||
TaskSnapshot mOpenSnapshot;
|
||||
boolean mIsLaunchBehind;
|
||||
|
||||
ScheduleAnimationBuilder(int type, BackAnimationAdapter backAnimationAdapter) {
|
||||
mType = type;
|
||||
mBackAnimationAdapter = backAnimationAdapter;
|
||||
}
|
||||
|
||||
ScheduleAnimationBuilder setComposeTarget(WindowContainer close, WindowContainer open) {
|
||||
mCloseTarget = close;
|
||||
mOpenTarget = open;
|
||||
return this;
|
||||
}
|
||||
|
||||
ScheduleAnimationBuilder setOpeningSnapshot(TaskSnapshot snapshot) {
|
||||
mOpenSnapshot = snapshot;
|
||||
return this;
|
||||
}
|
||||
|
||||
ScheduleAnimationBuilder setIsLaunchBehind(boolean launchBehind) {
|
||||
mIsLaunchBehind = launchBehind;
|
||||
return this;
|
||||
}
|
||||
|
||||
Runnable build() {
|
||||
if (mOpenTarget == null || mCloseTarget == null) {
|
||||
return null;
|
||||
}
|
||||
final boolean shouldLaunchBehind = mIsLaunchBehind || !isSupportWindowlessSurface();
|
||||
final ActivityRecord launchBehindActivity = !shouldLaunchBehind ? null
|
||||
: mOpenTarget.asTask() != null
|
||||
? mOpenTarget.asTask().getTopNonFinishingActivity()
|
||||
: mOpenTarget.asActivityRecord() != null
|
||||
? mOpenTarget.asActivityRecord() : null;
|
||||
if (shouldLaunchBehind && launchBehindActivity == null) {
|
||||
Slog.e(TAG, "No opening activity");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!composeAnimations(mCloseTarget, mOpenTarget)) {
|
||||
return null;
|
||||
}
|
||||
if (launchBehindActivity != null) {
|
||||
setLaunchBehind(launchBehindActivity);
|
||||
} else {
|
||||
createStartingSurface(mOpenSnapshot);
|
||||
}
|
||||
|
||||
final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback(
|
||||
launchBehindActivity != null ? triggerBack -> {
|
||||
if (!triggerBack) {
|
||||
restoreLaunchBehind(launchBehindActivity);
|
||||
}
|
||||
} : null,
|
||||
mCloseTarget);
|
||||
final RemoteAnimationTarget[] targets = getAnimationTargets();
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
mBackAnimationAdapter.getRunner().onAnimationStart(mType,
|
||||
targets, null, null, callback);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
backTargetTask = currentTask;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
// Find a screenshot of the previous activity if we actually have an animation
|
||||
SurfaceControl animationLeashParent = removedWindowContainer.getAnimationLeashParent();
|
||||
if (screenshotBuffer != null) {
|
||||
final SurfaceControl screenshotSurface = new SurfaceControl.Builder()
|
||||
.setName("BackPreview Screenshot for " + prevActivity)
|
||||
.setHidden(false)
|
||||
.setParent(animationLeashParent)
|
||||
.setBLASTLayer()
|
||||
.build();
|
||||
startedTransaction.setBuffer(screenshotSurface, screenshotBuffer);
|
||||
|
||||
// The Animation leash needs to be above the screenshot surface, but the animation
|
||||
// leash needs to be added before to be in the synchronized block.
|
||||
startedTransaction.setLayer(topAppTarget.leash, 1);
|
||||
|
||||
behindAppTarget =
|
||||
createRemoteAnimationTargetLocked(
|
||||
backTargetTask, screenshotSurface, MODE_OPENING);
|
||||
|
||||
// reset leash after animation finished.
|
||||
leashes.add(screenshotSurface);
|
||||
}
|
||||
} else if (prevTask != null && prevActivity != null) {
|
||||
// Make previous task show from behind by marking its top activity as visible
|
||||
// and launch-behind to bump its visibility for the duration of the back gesture.
|
||||
setLaunchBehind(prevActivity);
|
||||
|
||||
final SurfaceControl leash = prevActivity.makeAnimationLeash()
|
||||
.setName("BackPreview Leash for " + prevActivity)
|
||||
.setHidden(false)
|
||||
.build();
|
||||
prevActivity.reparentSurfaceControl(startedTransaction, leash);
|
||||
behindAppTarget = createRemoteAnimationTargetLocked(
|
||||
prevTask, leash, MODE_OPENING);
|
||||
|
||||
// reset leash after animation finished.
|
||||
leashes.add(leash);
|
||||
prevActivity.reparentSurfaceControl(finishedTransaction,
|
||||
prevActivity.getParentSurfaceControl());
|
||||
}
|
||||
|
||||
if (mShowWallpaper) {
|
||||
currentTask.getDisplayContent().mWallpaperController.adjustWallpaperWindows();
|
||||
// TODO(b/241808055): If the current animation need to show wallpaper and animate the
|
||||
// wallpaper, start the wallpaper animation to collect wallpaper target and deliver it
|
||||
// to the back animation controller.
|
||||
}
|
||||
|
||||
final RemoteAnimationTarget[] targets = (behindAppTarget == null)
|
||||
? new RemoteAnimationTarget[] {topAppTarget}
|
||||
: new RemoteAnimationTarget[] {topAppTarget, behindAppTarget};
|
||||
|
||||
final ActivityRecord finalPrevActivity = prevActivity;
|
||||
final IBackAnimationFinishedCallback callback =
|
||||
new IBackAnimationFinishedCallback.Stub() {
|
||||
private IBackAnimationFinishedCallback makeAnimationFinishedCallback(
|
||||
Consumer<Boolean> b, WindowContainer closeTarget) {
|
||||
return new IBackAnimationFinishedCallback.Stub() {
|
||||
@Override
|
||||
public void onAnimationFinished(boolean triggerBack) {
|
||||
for (SurfaceControl sc: leashes) {
|
||||
finishedTransaction.remove(sc);
|
||||
}
|
||||
final SurfaceControl.Transaction finishedTransaction =
|
||||
new SurfaceControl.Transaction();
|
||||
synchronized (mWindowManagerService.mGlobalLock) {
|
||||
if (b != null) {
|
||||
b.accept(triggerBack);
|
||||
}
|
||||
if (triggerBack) {
|
||||
final SurfaceControl surfaceControl =
|
||||
removedWindowContainer.getSurfaceControl();
|
||||
closeTarget.getSurfaceControl();
|
||||
if (surfaceControl != null && surfaceControl.isValid()) {
|
||||
// The animation is finish and start waiting for transition,
|
||||
// hide the task surface before it re-parented to avoid flicker.
|
||||
// Hide the close target surface when transition start.
|
||||
finishedTransaction.hide(surfaceControl);
|
||||
}
|
||||
} else if (!needsScreenshot(backType)) {
|
||||
restoreLaunchBehind(finalPrevActivity);
|
||||
}
|
||||
if (!mAnimationTargets.setFinishTransaction(finishedTransaction)) {
|
||||
if (!setFinishTransaction(finishedTransaction)) {
|
||||
finishedTransaction.apply();
|
||||
}
|
||||
if (!triggerBack) {
|
||||
mAnimationTargets.clearBackAnimateTarget(null);
|
||||
clearBackAnimateTarget(
|
||||
null /* cleanupTransaction */);
|
||||
} else {
|
||||
mAnimationTargets.mWaitTransition = true;
|
||||
mWaitTransition = true;
|
||||
}
|
||||
}
|
||||
// TODO Add timeout monitor if transition didn't happen
|
||||
}
|
||||
};
|
||||
if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
|
||||
mAnimationTargets.composeNewAnimations(removedWindowContainer, prevActivity);
|
||||
} else if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
|
||||
|| backType == BackNavigationInfo.TYPE_CROSS_TASK) {
|
||||
mAnimationTargets.composeNewAnimations(removedWindowContainer, prevTask);
|
||||
}
|
||||
scheduleAnimationLocked(backType, targets, adapter, callback);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static RemoteAnimationTarget createRemoteAnimationTargetLocked(
|
||||
Task task, SurfaceControl animLeash, int mode) {
|
||||
ActivityRecord topApp = task.getTopRealVisibleActivity();
|
||||
if (topApp == null) {
|
||||
topApp = task.getTopNonFinishingActivity();
|
||||
}
|
||||
|
||||
final WindowState mainWindow = topApp != null
|
||||
? topApp.findMainWindow()
|
||||
: null;
|
||||
int windowType = INVALID_WINDOW_TYPE;
|
||||
if (mainWindow != null) {
|
||||
windowType = mainWindow.getWindowType();
|
||||
}
|
||||
|
||||
Rect bounds = new Rect(task.getBounds());
|
||||
Rect localBounds = new Rect(bounds);
|
||||
Point tmpPos = new Point();
|
||||
task.getRelativePosition(tmpPos);
|
||||
localBounds.offsetTo(tmpPos.x, tmpPos.y);
|
||||
|
||||
return new RemoteAnimationTarget(
|
||||
task.mTaskId,
|
||||
mode,
|
||||
animLeash,
|
||||
false /* isTransluscent */,
|
||||
new Rect() /* clipRect */,
|
||||
new Rect() /* contentInsets */,
|
||||
task.getPrefixOrderIndex(),
|
||||
tmpPos /* position */,
|
||||
localBounds /* localBounds */,
|
||||
bounds /* screenSpaceBounds */,
|
||||
task.getWindowConfiguration(),
|
||||
true /* isNotInRecent */,
|
||||
null,
|
||||
null,
|
||||
task.getTaskInfo(),
|
||||
false,
|
||||
windowType);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void scheduleAnimationLocked(@BackNavigationInfo.BackTargetType int type,
|
||||
RemoteAnimationTarget[] targets, BackAnimationAdapter backAnimationAdapter,
|
||||
IBackAnimationFinishedCallback callback) {
|
||||
mPendingAnimation = () -> {
|
||||
try {
|
||||
backAnimationAdapter.getRunner().onAnimationStart(type,
|
||||
targets, null, null, callback);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
mWindowManagerService.mWindowPlacerLocked.requestTraversal();
|
||||
|
||||
private void setLaunchBehind(ActivityRecord activity) {
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
if (!activity.isVisibleRequested()) {
|
||||
activity.setVisibility(true);
|
||||
}
|
||||
activity.mLaunchTaskBehind = true;
|
||||
|
||||
// Handle fixed rotation launching app.
|
||||
final DisplayContent dc = activity.mDisplayContent;
|
||||
dc.rotateInDifferentOrientationIfNeeded(activity);
|
||||
if (activity.hasFixedRotationTransform()) {
|
||||
// Set the record so we can recognize it to continue to update display
|
||||
// orientation if the previous activity becomes the top later.
|
||||
dc.setFixedRotationLaunchingApp(activity,
|
||||
activity.getWindowConfiguration().getRotation());
|
||||
}
|
||||
|
||||
ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
|
||||
"Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
|
||||
activity.mTaskSupervisor.mStoppingActivities.remove(activity);
|
||||
activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
|
||||
0 /* configChanges */, false /* preserveWindows */, true);
|
||||
}
|
||||
private void restoreLaunchBehind(ActivityRecord activity) {
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
|
||||
|
||||
// Restore the launch-behind state.
|
||||
activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token);
|
||||
activity.mLaunchTaskBehind = false;
|
||||
ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
|
||||
"Setting Activity.mLauncherTaskBehind to false. Activity=%s",
|
||||
activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void checkAnimationReady(WallpaperController wallpaperController) {
|
||||
@@ -796,80 +965,36 @@ class BackNavigationController {
|
||||
mShowWallpaper = false;
|
||||
}
|
||||
|
||||
private HardwareBuffer getActivitySnapshot(@NonNull Task task, ActivityRecord r) {
|
||||
return task.getSnapshotForActivityRecord(r);
|
||||
}
|
||||
|
||||
private HardwareBuffer getTaskSnapshot(int taskId, int userId) {
|
||||
if (mWindowManagerService.mTaskSnapshotController == null) {
|
||||
private static TaskSnapshot getActivitySnapshot(@NonNull ActivityRecord r) {
|
||||
if (!isScreenshotEnabled()) {
|
||||
return null;
|
||||
}
|
||||
TaskSnapshot snapshot = mWindowManagerService.mTaskSnapshotController.getSnapshot(taskId,
|
||||
userId, true /* restoreFromDisk */, false /* isLowResolution */);
|
||||
return snapshot != null ? snapshot.getHardwareBuffer() : null;
|
||||
// Check if we have a screenshot of the previous activity, indexed by its
|
||||
// component name.
|
||||
// TODO return TaskSnapshot when feature complete.
|
||||
// final HardwareBuffer hw = r.getTask().getSnapshotForActivityRecord(r);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean needsScreenshot(int backType) {
|
||||
private static TaskSnapshot getTaskSnapshot(Task task) {
|
||||
if (!isScreenshotEnabled()) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
switch (backType) {
|
||||
case BackNavigationInfo.TYPE_RETURN_TO_HOME:
|
||||
case BackNavigationInfo.TYPE_DIALOG_CLOSE:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// Don't read from disk!!
|
||||
return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot(
|
||||
task.mTaskId, task.mUserId, false /* restoreFromDisk */,
|
||||
false /* isLowResolution */);
|
||||
}
|
||||
|
||||
void setWindowManager(WindowManagerService wm) {
|
||||
mWindowManagerService = wm;
|
||||
}
|
||||
|
||||
private void setLaunchBehind(ActivityRecord activity) {
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
if (!activity.isVisibleRequested()) {
|
||||
activity.setVisibility(true);
|
||||
}
|
||||
activity.mLaunchTaskBehind = true;
|
||||
|
||||
// Handle fixed rotation launching app.
|
||||
final DisplayContent dc = activity.mDisplayContent;
|
||||
dc.rotateInDifferentOrientationIfNeeded(activity);
|
||||
if (activity.hasFixedRotationTransform()) {
|
||||
// Set the record so we can recognize it to continue to update display orientation
|
||||
// if the previous activity becomes the top later.
|
||||
dc.setFixedRotationLaunchingApp(activity,
|
||||
activity.getWindowConfiguration().getRotation());
|
||||
}
|
||||
|
||||
ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
|
||||
"Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
|
||||
activity.mTaskSupervisor.mStoppingActivities.remove(activity);
|
||||
activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
|
||||
0 /* configChanges */, false /* preserveWindows */, true);
|
||||
}
|
||||
|
||||
private void restoreLaunchBehind(ActivityRecord activity) {
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activity.mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
|
||||
|
||||
// Restore the launch-behind state.
|
||||
activity.mTaskSupervisor.scheduleLaunchTaskBehindComplete(activity.token);
|
||||
activity.mLaunchTaskBehind = false;
|
||||
ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
|
||||
"Setting Activity.mLauncherTaskBehind to false. Activity=%s",
|
||||
activity);
|
||||
mAnimationHandler = new AnimationHandler(wm);
|
||||
}
|
||||
|
||||
boolean isWallpaperVisible(WindowState w) {
|
||||
return mAnimationTargets.mComposed && mShowWallpaper
|
||||
return mAnimationHandler.mComposed && mShowWallpaper
|
||||
&& w.mAttrs.type == TYPE_BASE_APPLICATION && w.mActivityRecord != null
|
||||
&& mAnimationTargets.isTarget(w.mActivityRecord, true /* open */);
|
||||
&& mAnimationHandler.isTarget(w.mActivityRecord, true /* open */);
|
||||
}
|
||||
|
||||
// Called from WindowManagerService to write to a protocol buffer output stream.
|
||||
|
||||
@@ -564,6 +564,11 @@ class SurfaceAnimator {
|
||||
*/
|
||||
public static final int ANIMATION_TYPE_STARTING_REVEAL = 1 << 7;
|
||||
|
||||
/**
|
||||
* Animation when a back gesture animation is applied to a window container.
|
||||
* @hide
|
||||
*/
|
||||
public static final int ANIMATION_TYPE_PREDICT_BACK = 1 << 8;
|
||||
/**
|
||||
* Bitmask to include all animation types. This is NOT an {@link AnimationType}
|
||||
* @hide
|
||||
@@ -583,7 +588,8 @@ class SurfaceAnimator {
|
||||
ANIMATION_TYPE_WINDOW_ANIMATION,
|
||||
ANIMATION_TYPE_INSETS_CONTROL,
|
||||
ANIMATION_TYPE_TOKEN_TRANSFORM,
|
||||
ANIMATION_TYPE_STARTING_REVEAL
|
||||
ANIMATION_TYPE_STARTING_REVEAL,
|
||||
ANIMATION_TYPE_PREDICT_BACK
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface AnimationType {}
|
||||
@@ -602,6 +608,7 @@ class SurfaceAnimator {
|
||||
case ANIMATION_TYPE_INSETS_CONTROL: return "insets_animation";
|
||||
case ANIMATION_TYPE_TOKEN_TRANSFORM: return "token_transform";
|
||||
case ANIMATION_TYPE_STARTING_REVEAL: return "starting_reveal";
|
||||
case ANIMATION_TYPE_PREDICT_BACK: return "predict_back";
|
||||
default: return "unknown type:" + type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,6 +722,14 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isSupportWindowlessStartingSurface() {
|
||||
// Enable after ag/20426257
|
||||
final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
|
||||
if (lastOrganizer == null) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Notify the shell ({@link com.android.wm.shell.ShellTaskOrganizer} that the client has
|
||||
* removed the splash screen view.
|
||||
|
||||
@@ -34,7 +34,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
@@ -96,9 +95,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
|
||||
.isEqualTo(typeToString(BackNavigationInfo.TYPE_RETURN_TO_HOME));
|
||||
|
||||
// verify if back animation would start.
|
||||
verify(mBackNavigationController).scheduleAnimationLocked(
|
||||
eq(BackNavigationInfo.TYPE_RETURN_TO_HOME), any(), eq(mBackAnimationAdapter),
|
||||
any());
|
||||
assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,9 +112,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
|
||||
.isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_TASK));
|
||||
|
||||
// verify if back animation would start.
|
||||
verify(mBackNavigationController).scheduleAnimationLocked(
|
||||
eq(BackNavigationInfo.TYPE_CROSS_TASK), any(), eq(mBackAnimationAdapter),
|
||||
any());
|
||||
assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation());
|
||||
|
||||
// reset drawning status
|
||||
topTask.forAllWindows(w -> {
|
||||
|
||||
Reference in New Issue
Block a user