diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 6d88d637dc241..482f2f07499d4 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -386,6 +386,16 @@ public interface WindowManager extends ViewManager { * @hide */ int TRANSIT_KEYGUARD_UNOCCLUDE = 9; + /** + * The first slot for custom transition types. Callers (like Shell) can make use of custom + * transition types for dealing with special cases. These types are effectively ignored by + * Core and will just be passed along as part of TransitionInfo objects. An example is + * split-screen using a custom type for it's snap-to-dismiss action. By using a custom type, + * Shell can properly dispatch the results of that transition to the split-screen + * implementation. + * @hide + */ + int TRANSIT_FIRST_CUSTOM = 10; /** * @hide @@ -401,6 +411,7 @@ public interface WindowManager extends ViewManager { TRANSIT_KEYGUARD_GOING_AWAY, TRANSIT_KEYGUARD_OCCLUDE, TRANSIT_KEYGUARD_UNOCCLUDE, + TRANSIT_FIRST_CUSTOM }) @Retention(RetentionPolicy.SOURCE) @interface TransitionType {} diff --git a/core/java/android/window/ITransitionPlayer.aidl b/core/java/android/window/ITransitionPlayer.aidl index a8a29b26a1485..55d47cb7ed8bf 100644 --- a/core/java/android/window/ITransitionPlayer.aidl +++ b/core/java/android/window/ITransitionPlayer.aidl @@ -16,6 +16,7 @@ package android.window; +import android.app.ActivityManager; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; @@ -58,6 +59,9 @@ oneway interface ITransitionPlayer { * @param type The {@link WindowManager#TransitionType} of the transition to start. * @param transitionToken An identifying token for the transition that needs to be started. * Pass this to {@link IWindowOrganizerController#startTransition}. + * @param triggerTask If non-null, the task containing the activity whose lifecycle change + * (start or finish) has caused this transition to occur. */ - void requestStartTransition(int type, in IBinder transitionToken); + void requestStartTransition(int type, in IBinder transitionToken, + in ActivityManager.RunningTaskInfo triggerTask); } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index da291cf0fd2cf..d1d49b6d47221 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.graphics.Point; import android.graphics.Rect; import android.os.Parcel; @@ -153,7 +154,8 @@ public final class TransitionInfo implements Parcelable { /** * @return a surfacecontrol that can serve as a parent surfacecontrol for all the changing * participants to animate within. This will generally be placed at the highest-z-order - * shared ancestor of all participants. + * shared ancestor of all participants. While this is non-null, it's possible for the rootleash + * to be invalid if the transition is a no-op. */ @NonNull public SurfaceControl getRootLeash() { @@ -181,7 +183,7 @@ public final class TransitionInfo implements Parcelable { @Nullable public Change getChange(@NonNull WindowContainerToken token) { for (int i = mChanges.size() - 1; i >= 0; --i) { - if (mChanges.get(i).mContainer == token) { + if (token.equals(mChanges.get(i).mContainer)) { return mChanges.get(i); } } @@ -254,6 +256,7 @@ public final class TransitionInfo implements Parcelable { private final Rect mStartAbsBounds = new Rect(); private final Rect mEndAbsBounds = new Rect(); private final Point mEndRelOffset = new Point(); + private ActivityManager.RunningTaskInfo mTaskInfo = null; public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) { mContainer = container; @@ -270,6 +273,7 @@ public final class TransitionInfo implements Parcelable { mStartAbsBounds.readFromParcel(in); mEndAbsBounds.readFromParcel(in); mEndRelOffset.readFromParcel(in); + mTaskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR); } /** Sets the parent of this change's container. The parent must be a participant or null. */ @@ -302,6 +306,14 @@ public final class TransitionInfo implements Parcelable { mEndRelOffset.set(left, top); } + /** + * Sets the taskinfo of this container if this is a task. WARNING: this takes the + * reference, so don't modify it afterwards. + */ + public void setTaskInfo(ActivityManager.RunningTaskInfo taskInfo) { + mTaskInfo = taskInfo; + } + /** @return the container that is changing. May be null if non-remotable (eg. activity) */ @Nullable public WindowContainerToken getContainer() { @@ -359,6 +371,12 @@ public final class TransitionInfo implements Parcelable { return mLeash; } + /** @return the task info or null if this isn't a task */ + @NonNull + public ActivityManager.RunningTaskInfo getTaskInfo() { + return mTaskInfo; + } + @Override /** @hide */ public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -370,6 +388,7 @@ public final class TransitionInfo implements Parcelable { mStartAbsBounds.writeToParcel(dest, flags); mEndAbsBounds.writeToParcel(dest, flags); mEndRelOffset.writeToParcel(dest, flags); + dest.writeTypedObject(mTaskInfo, flags); } @NonNull diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java index 63d31182a7485..8817f8aaec7c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java @@ -55,19 +55,16 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", taskInfo.taskId); mLeashByTaskId.put(taskInfo.taskId, leash); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; final Point positionInParent = taskInfo.positionInParent; mSyncQueue.runInSync(t -> { // Reset several properties back to fullscreen (PiP, for example, leaves all these // properties in a bad state). t.setWindowCrop(leash, null); t.setPosition(leash, positionInParent.x, positionInParent.y); - // TODO(shell-transitions): Eventually set everything in transition so there's no - // SF Transaction here. - if (!Transitions.ENABLE_SHELL_TRANSITIONS) { - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); - } + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java index 54863d23643a2..5213f6c80989c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java @@ -16,6 +16,7 @@ package com.android.wm.shell; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -25,15 +26,17 @@ import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPI import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; import android.util.ArrayMap; -import android.util.Slog; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.ITransitionPlayer; import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; import androidx.annotation.BinderThread; @@ -59,8 +62,16 @@ public class Transitions { private final ShellExecutor mAnimExecutor; private final TransitionPlayerImpl mPlayerImpl; + /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ + private final ArrayList mHandlers = new ArrayList<>(); + + private static final class ActiveTransition { + ArrayList mAnimations = null; + TransitionHandler mFirstHandler = null; + } + /** Keeps track of currently tracked transitions and all the animations associated with each */ - private final ArrayMap> mActiveTransitions = new ArrayMap<>(); + private final ArrayMap mActiveTransitions = new ArrayMap<>(); public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { @@ -75,6 +86,22 @@ public class Transitions { taskOrganizer.registerTransitionPlayer(mPlayerImpl); } + /** + * Adds a handler candidate. + * @see TransitionHandler + */ + public void addHandler(@NonNull TransitionHandler handler) { + mHandlers.add(handler); + } + + public ShellExecutor getMainExecutor() { + return mMainExecutor; + } + + public ShellExecutor getAnimExecutor() { + return mAnimExecutor; + } + // TODO(shell-transitions): real animations private void startExampleAnimation(@NonNull IBinder transition, @NonNull SurfaceControl leash, boolean show) { @@ -93,7 +120,7 @@ public class Transitions { transaction.apply(); mTransactionPool.release(transaction); mMainExecutor.execute(() -> { - mActiveTransitions.get(transition).remove(va); + mActiveTransitions.get(transition).mAnimations.remove(va); onFinish(transition); }); }; @@ -114,30 +141,23 @@ public class Transitions { @Override public void onAnimationRepeat(Animator animation) { } }); - mActiveTransitions.get(transition).add(va); + mActiveTransitions.get(transition).mAnimations.add(va); mAnimExecutor.execute(va::start); } - private static boolean isOpeningType(@WindowManager.TransitionType int type) { + /** @return true if the transition was triggered by opening something vs closing something */ + public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; } - private void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, + /** + * Reparents all participants into a shared parent and orders them based on: the global transit + * type, their transit mode, and their destination z-order. + */ + private static void setupStartState(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", - transitionToken, info); - // start task - if (!mActiveTransitions.containsKey(transitionToken)) { - Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken - + " expecting one of " + mActiveTransitions.keySet()); - } - if (mActiveTransitions.get(transitionToken) != null) { - throw new IllegalStateException("Got a duplicate onTransitionReady call for " - + transitionToken); - } - mActiveTransitions.put(transitionToken, new ArrayList<>()); boolean isOpening = isOpeningType(info.getType()); if (info.getRootLeash().isValid()) { t.show(info.getRootLeash()); @@ -148,24 +168,26 @@ public class Transitions { final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); - // Don't animate anything with an animating parent + // Don't move anything with an animating parent if (change.getParent() != null) { - if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) { t.show(leash); t.setMatrix(leash, 1, 0, 0, 1); + t.setAlpha(leash, 1.f); + t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); } continue; } t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getEndAbsBounds().left - info.getRootOffset().x, - change.getEndAbsBounds().top - info.getRootOffset().y); + t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, + change.getStartAbsBounds().top - info.getRootOffset().y); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { t.show(leash); t.setMatrix(leash, 1, 0, 0, 1); if (isOpening) { - // put on top and fade in + // put on top with 0 alpha t.setLayer(leash, info.getChanges().size() - i); if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { // This received a transferred starting window, so make it immediately @@ -173,47 +195,155 @@ public class Transitions { t.setAlpha(leash, 1.f); } else { t.setAlpha(leash, 0.f); - startExampleAnimation(transitionToken, leash, true /* show */); } } else { - // put on bottom and leave it visible without fade + // put on bottom and leave it visible t.setLayer(leash, -i); t.setAlpha(leash, 1.f); } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { if (isOpening) { - // put on bottom and leave visible without fade + // put on bottom and leave visible t.setLayer(leash, -i); } else { - // put on top and fade out + // put on top t.setLayer(leash, info.getChanges().size() - i); - startExampleAnimation(transitionToken, leash, false /* show */); } - } else { + } else { // CHANGE t.setLayer(leash, info.getChanges().size() - i); } } + } + + private void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", + transitionToken, info); + final ActiveTransition active = mActiveTransitions.get(transitionToken); + if (active == null) { + throw new IllegalStateException("Got transitionReady for non-active transition " + + transitionToken + ". expecting one of " + mActiveTransitions.keySet()); + } + if (active.mAnimations != null) { + throw new IllegalStateException("Got a duplicate onTransitionReady call for " + + transitionToken); + } + if (!info.getRootLeash().isValid()) { + // Invalid root-leash implies that the transition is empty/no-op, so just do + // housekeeping and return. + t.apply(); + onFinish(transitionToken); + return; + } + + setupStartState(info, t); + + final Runnable finishRunnable = () -> onFinish(transitionToken); + // If a handler chose to uniquely run this animation, try delegating to it. + if (active.mFirstHandler != null && active.mFirstHandler.startAnimation( + transitionToken, info, t, finishRunnable)) { + return; + } + // Otherwise give every other handler a chance (in order) + for (int i = mHandlers.size() - 1; i >= 0; --i) { + if (mHandlers.get(i) == active.mFirstHandler) continue; + if (mHandlers.get(i).startAnimation(transitionToken, info, t, finishRunnable)) { + return; + } + } + + // No handler chose to perform this animation, so fall-back to the + // default animation handling. + final boolean isOpening = isOpeningType(info.getType()); + active.mAnimations = new ArrayList<>(); // Play fade animations + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Don't animate anything with an animating parent + if (change.getParent() != null) continue; + + final int mode = info.getChanges().get(i).getMode(); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + // This received a transferred starting window, so don't animate + continue; + } + // fade in + startExampleAnimation(transitionToken, change.getLeash(), true /* show */); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + startExampleAnimation(transitionToken, change.getLeash(), false /* show */); + } + } t.apply(); onFinish(transitionToken); } private void onFinish(IBinder transition) { - if (!mActiveTransitions.get(transition).isEmpty()) return; + final ActiveTransition active = mActiveTransitions.get(transition); + if (active.mAnimations != null && !active.mAnimations.isEmpty()) return; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animations finished, notifying core %s", transition); mActiveTransitions.remove(transition); mOrganizer.finishTransition(transition, null, null); } - private void requestStartTransition(int type, @NonNull IBinder transitionToken) { + private void requestStartTransition(int type, @NonNull IBinder transitionToken, + @Nullable ActivityManager.RunningTaskInfo triggerTask) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: type=%d %s", type, transitionToken); if (mActiveTransitions.containsKey(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } - IBinder transition = mOrganizer.startTransition(type, transitionToken, null /* wct */); - mActiveTransitions.put(transition, null); + final ActiveTransition active = new ActiveTransition(); + WindowContainerTransaction wct = null; + for (int i = mHandlers.size() - 1; i >= 0; --i) { + wct = mHandlers.get(i).handleRequest(type, transitionToken, triggerTask); + if (wct != null) { + active.mFirstHandler = mHandlers.get(i); + break; + } + } + IBinder transition = mOrganizer.startTransition(type, transitionToken, wct); + mActiveTransitions.put(transition, active); + } + + /** Start a new transition directly. */ + public IBinder startTransition(@WindowManager.TransitionType int type, + @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { + final ActiveTransition active = new ActiveTransition(); + active.mFirstHandler = handler; + IBinder transition = mOrganizer.startTransition(type, null /* token */, wct); + mActiveTransitions.put(transition, active); + return transition; + } + + /** + * Interface for something which can handle a subset of transitions. + */ + public interface TransitionHandler { + /** + * Starts a transition animation. This is always called if handleRequest returned non-null + * for a particular transition. Otherwise, it is only called if no other handler before + * it handled the transition. + * + * @return true if transition was handled, false if not (falls-back to default). + */ + boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback); + + /** + * Potentially handles a startTransition request. + * @param type The transition type + * @param triggerTask The task which triggered this transition request. + * @return WCT to apply with transition-start or null if this handler isn't handling + * the request. + */ + @Nullable + WindowContainerTransaction handleRequest(@WindowManager.TransitionType int type, + @NonNull IBinder transition, + @Nullable ActivityManager.RunningTaskInfo triggerTask); } @BinderThread @@ -227,9 +357,10 @@ public class Transitions { } @Override - public void requestStartTransition(int i, IBinder iBinder) throws RemoteException { + public void requestStartTransition(int i, IBinder iBinder, + ActivityManager.RunningTaskInfo runningTaskInfo) throws RemoteException { mMainExecutor.execute(() -> { - Transitions.this.requestStartTransition(i, iBinder); + Transitions.this.requestStartTransition(i, iBinder, runningTaskInfo); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java index 6b79a3661bc07..789bd1a90731f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -44,6 +44,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.Transitions; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -113,7 +114,7 @@ public class LegacySplitScreenController implements LegacySplitScreen, DisplayController displayController, SystemWindows systemWindows, DisplayImeController imeController, Handler handler, TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, - TaskStackListenerImpl taskStackListener) { + TaskStackListenerImpl taskStackListener, Transitions transitions) { mContext = context; mDisplayController = displayController; mSystemWindows = systemWindows; @@ -123,7 +124,8 @@ public class LegacySplitScreenController implements LegacySplitScreen, mTransactionPool = transactionPool; mWindowManagerProxy = new WindowManagerProxy(syncQueue, shellTaskOrganizer); mTaskOrganizer = shellTaskOrganizer; - mSplits = new LegacySplitScreenTaskListener(this, shellTaskOrganizer, syncQueue); + mSplits = new LegacySplitScreenTaskListener(this, shellTaskOrganizer, transitions, + syncQueue); mImePositionProcessor = new DividerImeController(mSplits, mTransactionPool, mHandler, shellTaskOrganizer); mRotationController = @@ -553,8 +555,36 @@ public class LegacySplitScreenController implements LegacySplitScreen, mHomeStackResizable = mWindowManagerProxy.applyEnterSplit(mSplits, mSplitLayout); } + void prepareEnterSplitTransition(WindowContainerTransaction outWct) { + // Set resizable directly here because buildEnterSplit already resizes home stack. + mHomeStackResizable = mWindowManagerProxy.buildEnterSplit(outWct, mSplits, mSplitLayout); + } + + void finishEnterSplitTransition(boolean minimized) { + update(mDisplayController.getDisplayContext( + mContext.getDisplayId()).getResources().getConfiguration()); + if (minimized) { + ensureMinimizedSplit(); + } else { + ensureNormalSplit(); + } + } + void startDismissSplit(boolean toPrimaryTask) { + startDismissSplit(toPrimaryTask, false /* snapped */); + } + + void startDismissSplit(boolean toPrimaryTask, boolean snapped) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mSplits.getSplitTransitions().dismissSplit( + mSplits, mSplitLayout, !toPrimaryTask, snapped); + } else { mWindowManagerProxy.applyDismissSplit(mSplits, mSplitLayout, !toPrimaryTask); + onDismissSplit(); + } + } + + void onDismissSplit() { updateVisibility(false /* visible */); mMinimized = false; // Resets divider bar position to undefined, so new divider bar will apply default position diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java index 02c82dee8ca46..8c624cdb10639 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTaskListener.java @@ -32,6 +32,7 @@ import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.TaskOrganizer; import androidx.annotation.NonNull; @@ -63,11 +64,17 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { final SurfaceSession mSurfaceSession = new SurfaceSession(); + private final SplitScreenTransitions mSplitTransitions; + LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, ShellTaskOrganizer shellTaskOrganizer, + Transitions transitions, SyncTransactionQueue syncQueue) { mSplitScreenController = splitScreenController; mTaskOrganizer = shellTaskOrganizer; + mSplitTransitions = new SplitScreenTransitions(splitScreenController.mTransactionPool, + transitions, mSplitScreenController, this); + transitions.addHandler(mSplitTransitions); mSyncQueue = syncQueue; } @@ -98,6 +105,14 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { mSplitScreenController.mTransactionPool.release(t); } + TaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + SplitScreenTransitions getSplitTransitions() { + return mSplitTransitions; + } + @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { synchronized (this) { @@ -195,10 +210,12 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { mLeashByTaskId.put(taskInfo.taskId, leash); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); } private void handleChildTaskChanged(RunningTaskInfo taskInfo) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); } @@ -241,14 +258,15 @@ class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { } else if (info.token.asBinder() == mSecondary.token.asBinder()) { mSecondary = info; } + if (DEBUG) { + Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); + } + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS && mSplitScreenController.isHomeStackResizable()); - if (DEBUG) { - Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); - } if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty && secondaryImpliedMinimize == secondaryImpliesMinimize) { // No relevant changes diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/SplitScreenTransitions.java new file mode 100644 index 0000000000000..93520c0b59dee --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/SplitScreenTransitions.java @@ -0,0 +1,342 @@ +/* + * 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.wm.shell.legacysplitscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.Transitions; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ExternalThread; + +import java.util.ArrayList; + +/** Plays transition animations for split-screen */ +public class SplitScreenTransitions implements Transitions.TransitionHandler { + private static final String TAG = "SplitScreenTransitions"; + + public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10; + + private final TransactionPool mTransactionPool; + private final Transitions mTransitions; + private final LegacySplitScreenController mSplitScreen; + private final LegacySplitScreenTaskListener mListener; + + private IBinder mPendingDismiss = null; + private boolean mDismissFromSnap = false; + private IBinder mPendingEnter = null; + private IBinder mAnimatingTransition = null; + + /** Keeps track of currently running animations */ + private final ArrayList mAnimations = new ArrayList<>(); + + private Runnable mFinishCallback = null; + private SurfaceControl.Transaction mFinishTransaction; + + SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, + @NonNull LegacySplitScreenController splitScreen, + @NonNull LegacySplitScreenTaskListener listener) { + mTransactionPool = pool; + mTransitions = transitions; + mSplitScreen = splitScreen; + mListener = listener; + } + + @Override + public WindowContainerTransaction handleRequest(@WindowManager.TransitionType int type, + @NonNull IBinder transition, @Nullable ActivityManager.RunningTaskInfo triggerTask) { + WindowContainerTransaction out = null; + if (mSplitScreen.isDividerVisible()) { + // try to handle everything while in split-screen + out = new WindowContainerTransaction(); + if (triggerTask != null) { + final boolean shouldDismiss = + // if we close the primary-docked task, then leave split-screen since there + // is nothing behind it. + ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK) + && triggerTask.parentTaskId == mListener.mPrimary.taskId) + // if a non-resizable is launched, we also need to leave split-screen. + || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) + && !triggerTask.isResizeable); + // In both cases, dismiss the primary + if (shouldDismiss) { + WindowManagerProxy.buildDismissSplit(out, mListener, + mSplitScreen.getSplitLayout(), true /* dismiss */); + if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) { + out.reorder(triggerTask.token, true /* onTop */); + } + mPendingDismiss = transition; + } + } + } else if (triggerTask != null) { + // Not in split mode, so look for an open with a trigger task. + if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) + && triggerTask.configuration.windowConfiguration.getWindowingMode() + == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { + out = new WindowContainerTransaction(); + mSplitScreen.prepareEnterSplitTransition(out); + mPendingEnter = transition; + } + } + return out; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + // TODO(shell-transitions): real animations + private void startExampleResizeAnimation(@NonNull SurfaceControl leash, + @NonNull Rect startBounds, @NonNull Rect endBounds) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setWindowCrop(leash, + (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), + (int) (startBounds.height() * (1.f - fraction) + + endBounds.height() * fraction)); + transaction.setPosition(leash, + startBounds.left * (1.f - fraction) + endBounds.left * fraction, + startBounds.top * (1.f - fraction) + endBounds.top * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setWindowCrop(leash, 0, 0); + transaction.setPosition(leash, endBounds.left, endBounds.top); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { + if (transition != mPendingDismiss && transition != mPendingEnter) { + // If we're not in split-mode, just abort + if (!mSplitScreen.isDividerVisible()) return false; + // Check to see if HOME is involved + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null + || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue; + if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { + mSplitScreen.ensureMinimizedSplit(); + } else if (change.getMode() == TRANSIT_CLOSE + || change.getMode() == TRANSIT_TO_BACK) { + mSplitScreen.ensureNormalSplit(); + } + } + // Use normal animations. + return false; + } + + mFinishCallback = finishCallback; + mFinishTransaction = mTransactionPool.acquire(); + mAnimatingTransition = transition; + + // Play fade animations + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + if (mode == TRANSIT_CHANGE) { + if (change.getParent() != null) { + // This is probably reparented, so we want the parent to be immediately visible + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + t.show(parentChange.getLeash()); + t.setAlpha(parentChange.getLeash(), 1.f); + // and then animate this layer outside the parent (since, for example, this is + // the home task animating from fullscreen to part-screen). + t.reparent(leash, info.getRootLeash()); + t.setLayer(leash, info.getChanges().size() - i); + // build the finish reparent/reposition + mFinishTransaction.reparent(leash, parentChange.getLeash()); + mFinishTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + } + // TODO(shell-transitions): screenshot here + final Rect startBounds = new Rect(change.getStartAbsBounds()); + final boolean isHome = change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; + if (mPendingDismiss == transition && mDismissFromSnap && !isHome) { + // Home is special since it doesn't move during fling. Everything else, though, + // when dismissing from snap, the top/left is at 0,0. + startBounds.offsetTo(0, 0); + } + final Rect endBounds = new Rect(change.getEndAbsBounds()); + startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + startExampleResizeAnimation(leash, startBounds, endBounds); + } + if (change.getParent() != null) { + continue; + } + + if (transition == mPendingEnter + && mListener.mPrimary.token.equals(change.getContainer()) + || mListener.mSecondary.token.equals(change.getContainer())) { + t.setWindowCrop(leash, change.getStartAbsBounds().width(), + change.getStartAbsBounds().height()); + if (mListener.mPrimary.token.equals(change.getContainer())) { + // Move layer to top since we want it above the oversized home task during + // animation even though home task is on top in hierarchy. + t.setLayer(leash, info.getChanges().size() + 1); + } + } + boolean isOpening = Transitions.isOpeningType(info.getType()); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + // fade in + startExampleAnimation(leash, true /* show */); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + if (transition == mPendingDismiss && mDismissFromSnap) { + // Dismissing via snap-to-top/bottom means that the dismissed task is already + // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 + // and don't animate it so it doesn't pop-in when reparented. + t.setAlpha(leash, 0.f); + } else { + startExampleAnimation(leash, false /* show */); + } + } + } + if (transition == mPendingEnter) { + // If entering, check if we should enter into minimized or normal split + boolean homeIsVisible = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null + || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) { + continue; + } + homeIsVisible = change.getMode() == TRANSIT_OPEN + || change.getMode() == TRANSIT_TO_FRONT + || change.getMode() == TRANSIT_CHANGE; + break; + } + mSplitScreen.finishEnterSplitTransition(homeIsVisible); + } + t.apply(); + onFinish(); + return true; + } + + @ExternalThread + void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + boolean dismissOrMaximize, boolean snapped) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize); + mTransitions.getMainExecutor().execute(() -> { + mDismissFromSnap = snapped; + mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this); + }); + } + + private void onFinish() { + if (!mAnimations.isEmpty()) return; + mFinishTransaction.apply(); + mTransactionPool.release(mFinishTransaction); + mFinishTransaction = null; + mFinishCallback.run(); + mFinishCallback = null; + if (mAnimatingTransition == mPendingEnter) { + mPendingEnter = null; + } + if (mAnimatingTransition == mPendingDismiss) { + mSplitScreen.onDismissSplit(); + mPendingDismiss = null; + } + mDismissFromSnap = false; + mAnimatingTransition = null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java index 68da35d9c4b1e..90a8de02deb8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/WindowManagerProxy.java @@ -39,6 +39,7 @@ import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; import com.android.internal.annotations.GuardedBy; +import com.android.wm.shell.Transitions; import com.android.wm.shell.common.SyncTransactionQueue; import java.util.ArrayList; @@ -90,7 +91,11 @@ class WindowManagerProxy { void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) { - mExecutor.execute(() -> applyDismissSplit(tiles, layout, dismissOrMaximize)); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */); + } else { + mExecutor.execute(() -> applyDismissSplit(tiles, layout, dismissOrMaximize)); + } } public void setResizing(final boolean resizing) { @@ -181,6 +186,18 @@ class WindowManagerProxy { return isHomeResizable; } + /** @see #buildEnterSplit */ + boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) { + // Set launchtile first so that any stack created after + // getAllRootTaskInfos and before reparent (even if unlikely) are placed + // correctly. + mTaskOrganizer.setLaunchRoot(DEFAULT_DISPLAY, tiles.mSecondary.token); + WindowContainerTransaction wct = new WindowContainerTransaction(); + final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout); + applySyncTransaction(wct); + return isHomeResizable; + } + /** * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split. * This assumes there is already something in the primary split since that is usually what @@ -189,14 +206,10 @@ class WindowManagerProxy { * * @return whether the home stack is resizable */ - boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) { - // Set launchtile first so that any stack created after - // getAllRootTaskInfos and before reparent (even if unlikely) are placed - // correctly. - mTaskOrganizer.setLaunchRoot(DEFAULT_DISPLAY, tiles.mSecondary.token); + boolean buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, + LegacySplitDisplayLayout layout) { List rootTasks = mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */); - WindowContainerTransaction wct = new WindowContainerTransaction(); if (rootTasks.isEmpty()) { return false; } @@ -215,48 +228,60 @@ class WindowManagerProxy { // Since this iterates from bottom to top, update topHomeTask for every fullscreen task // so it will be left with the status of the top one. topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null; - wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); + outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); } // Move the secondary split-forward. - wct.reorder(tiles.mSecondary.token, true /* onTop */); - boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); - if (topHomeTask != null) { + outWct.reorder(tiles.mSecondary.token, true /* onTop */); + boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, + outWct); + if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) { // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST // is enabled, this temporarily syncs the home surface position with offset until // sync transaction finishes. - wct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); + outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); } - applySyncTransaction(wct); return isHomeResizable; } - boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { + static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { final int atype = ti.getActivityType(); return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; } + /** @see #buildDismissSplit */ + void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + boolean dismissOrMaximize) { + // Set launch root first so that any task created after getChildContainers and + // before reparent (pretty unlikely) are put into fullscreen. + mTaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null); + // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished + // plus specific APIs to clean this up. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + buildDismissSplit(wct, tiles, layout, dismissOrMaximize); + applySyncTransaction(wct); + } + /** * Reparents all tile members back to their display and resets home task override bounds. * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary * split (thus resulting in the top of the secondary split becoming * fullscreen. {@code false} resolves the other way. */ - void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, + static void buildDismissSplit(WindowContainerTransaction outWct, + LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize) { - // Set launch root first so that any task created after getChildContainers and - // before reparent (pretty unlikely) are put into fullscreen. - mTaskOrganizer.setLaunchRoot(Display.DEFAULT_DISPLAY, null); // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished // plus specific APIs to clean this up. + final TaskOrganizer taskOrg = tiles.getTaskOrganizer(); List primaryChildren = - mTaskOrganizer.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); + taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); List secondaryChildren = - mTaskOrganizer.getChildTasks(tiles.mSecondary.token, null /* activityTypes */); + taskOrg.getChildTasks(tiles.mSecondary.token, null /* activityTypes */); // In some cases (eg. non-resizable is launched), system-server will leave split-screen. // as a result, the above will not capture any tasks; yet, we need to clean-up the // home task bounds. List freeHomeAndRecents = - mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS); + taskOrg.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS); // Filter out the root split tasks freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token) || p.token.equals(tiles.mPrimary.token)); @@ -265,11 +290,10 @@ class WindowManagerProxy { && freeHomeAndRecents.isEmpty()) { return; } - WindowContainerTransaction wct = new WindowContainerTransaction(); if (dismissOrMaximize) { // Dismissing, so move all primary split tasks first for (int i = primaryChildren.size() - 1; i >= 0; --i) { - wct.reparent(primaryChildren.get(i).token, null /* parent */, + outWct.reparent(primaryChildren.get(i).token, null /* parent */, true /* onTop */); } boolean homeOnTop = false; @@ -277,16 +301,16 @@ class WindowManagerProxy { // order within the secondary split. for (int i = secondaryChildren.size() - 1; i >= 0; --i) { final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); - wct.reparent(ti.token, null /* parent */, true /* onTop */); + outWct.reparent(ti.token, null /* parent */, true /* onTop */); if (isHomeOrRecentTask(ti)) { - wct.setBounds(ti.token, null); - wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + outWct.setBounds(ti.token, null); + outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); if (i == 0) { homeOnTop = true; } } } - if (homeOnTop) { + if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) { // Translate/update-crop of secondary out-of-band with sync transaction -- instead // play this in sync with new home-app frame because until BALST is enabled this // shows up on screen before the syncTransaction returns. @@ -304,7 +328,7 @@ class WindowManagerProxy { layout.mDisplayLayout.height()); crop.offset(-posX, -posY); sft.setWindowCrop(tiles.mSecondarySurface, crop); - wct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); + outWct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); } } else { // Maximize, so move non-home secondary split first @@ -312,7 +336,7 @@ class WindowManagerProxy { if (isHomeOrRecentTask(secondaryChildren.get(i))) { continue; } - wct.reparent(secondaryChildren.get(i).token, null /* parent */, + outWct.reparent(secondaryChildren.get(i).token, null /* parent */, true /* onTop */); } // Find and place home tasks in-between. This simulates the fact that there was @@ -320,24 +344,23 @@ class WindowManagerProxy { for (int i = secondaryChildren.size() - 1; i >= 0; --i) { final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); if (isHomeOrRecentTask(ti)) { - wct.reparent(ti.token, null /* parent */, true /* onTop */); + outWct.reparent(ti.token, null /* parent */, true /* onTop */); // reset bounds and mode too - wct.setBounds(ti.token, null); - wct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); + outWct.setBounds(ti.token, null); + outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); } } for (int i = primaryChildren.size() - 1; i >= 0; --i) { - wct.reparent(primaryChildren.get(i).token, null /* parent */, + outWct.reparent(primaryChildren.get(i).token, null /* parent */, true /* onTop */); } } for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) { - wct.setBounds(freeHomeAndRecents.get(i).token, null); - wct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); + outWct.setBounds(freeHomeAndRecents.get(i).token, null); + outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); } // Reset focusable to true - wct.setFocusable(tiles.mPrimary.token, true /* focusable */); - applySyncTransaction(wct); + outWct.setFocusable(tiles.mPrimary.token, true /* focusable */); } /** diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 926062b1623fe..f9505dec78fd8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -28,9 +28,9 @@ import com.android.wm.shell.ShellCommandHandler; import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index a1bde589d5c14..943a54e61c03f 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -22,9 +22,9 @@ import com.android.wm.shell.ShellInit; import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import java.util.Optional; diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 6d9d587bc729a..54e30af675ab5 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -91,11 +91,11 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarWindowCallback; import com.android.systemui.statusbar.policy.CallbackController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEvents; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import java.io.FileDescriptor; import java.io.PrintWriter; diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java index 765fd32f7cd23..1049548c382c4 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java @@ -23,6 +23,7 @@ import android.view.IWindowManager; import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.Transitions; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.SyncTransactionQueue; @@ -58,9 +59,10 @@ public class TvWMShellModule { DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, @Main Handler handler, TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener) { + SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener, + Transitions transitions) { return new LegacySplitScreenController(context, displayController, systemWindows, displayImeController, handler, transactionPool, shellTaskOrganizer, syncQueue, - taskStackListener); + taskStackListener, transitions); } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index 3399a0bfc72e6..9ec7657c5dd51 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -23,6 +23,7 @@ import android.view.IWindowManager; import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.Transitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.apppairs.AppPairsController; @@ -35,6 +36,8 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; @@ -46,8 +49,6 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.pip.phone.PipAppOpsListener; import com.android.wm.shell.pip.phone.PipController; import com.android.wm.shell.pip.phone.PipTouchHandler; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import java.util.Optional; import java.util.concurrent.Executor; @@ -76,10 +77,11 @@ public class WMShellModule { DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, @Main Handler handler, TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, - SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener) { + SyncTransactionQueue syncQueue, TaskStackListenerImpl taskStackListener, + Transitions transitions) { return new LegacySplitScreenController(context, displayController, systemWindows, displayImeController, handler, transactionPool, shellTaskOrganizer, syncQueue, - taskStackListener); + taskStackListener, transitions); } @WMSingleton diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.java index b3176ddeaf656..4d32a3b4077f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.java @@ -40,8 +40,8 @@ import com.android.systemui.shared.recents.IPinnedStackAnimationListener; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.wm.shell.pip.Pip; import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.pip.Pip; import org.junit.Before; import org.junit.Test; diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 73d87b0d45963..31bf7120900fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -35,12 +35,12 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.ShellCommandHandler; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedGestureHandler; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.phone.PipTouchHandler; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; import org.junit.Before; import org.junit.Test; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 5289f8604f361..375c3e138a398 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1618,7 +1618,8 @@ class ActivityStarter { mService.getTransitionController().collectExistenceChange(r); } if (newTransition != null) { - mService.getTransitionController().requestStartTransition(newTransition); + mService.getTransitionController().requestStartTransition(newTransition, + r.getTask()); } else { // Make the collecting transition wait until this request is ready. mService.getTransitionController().setReady(false); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 9d291b12c2c93..73a6efdb67997 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2207,6 +2207,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } if (!task.supportsSplitScreenWindowingMode() || forceNonResizable) { + if (mService.getTransitionController().getTransitionPlayer() != null) return; // Dismiss docked stack. If task appeared to be in docked stack but is not resizable - // we need to move it to top of fullscreen stack, otherwise it will be covered. final TaskDisplayArea taskDisplayArea = task.getDisplayArea(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index be78d7a18377c..b37e3c42f568a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -37,6 +37,7 @@ import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.ActivityManager; import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; @@ -567,6 +568,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe tmpList.add(wc); } for (WindowContainer p = wc.getParent(); p != null; p = p.getParent()) { + if (!p.isAttached() || !changes.get(p).hasChanged(p)) { + // Again, we're skipping no-ops + break; + } if (participants.contains(p)) { topParent = p; break; @@ -695,6 +700,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final TransitionInfo.Change change = new TransitionInfo.Change( target.mRemoteToken != null ? target.mRemoteToken.toWindowContainerToken() : null, target.getSurfaceControl()); + // TODO(shell-transitions): Use leash for non-organized windows. if (info.mParent != null) { change.setParent(info.mParent.mRemoteToken.toWindowContainerToken()); } @@ -704,6 +710,12 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe change.setEndRelOffset(target.getBounds().left - target.getParent().getBounds().left, target.getBounds().top - target.getParent().getBounds().top); change.setFlags(info.getChangeFlags(target)); + final Task task = target.asTask(); + if (task != null) { + final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo(); + task.fillTaskInfo(tinfo); + change.setTaskInfo(tinfo); + } out.addChange(change); } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 2f5d10afe3dae..0fe0afa54dd38 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -167,7 +168,8 @@ class TransitionController { // Make the collecting transition wait until this request is ready. mCollectingTransition.setReady(false); } else { - newTransition = requestStartTransition(createTransition(type, flags)); + newTransition = requestStartTransition(createTransition(type, flags), + trigger != null ? trigger.asTask() : null); } if (trigger != null) { if (isExistenceType(type)) { @@ -181,11 +183,16 @@ class TransitionController { /** Asks the transition player (shell) to start a created but not yet started transition. */ @NonNull - Transition requestStartTransition(@NonNull Transition transition) { + Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask) { try { ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Requesting StartTransition: %s", transition); - mTransitionPlayer.requestStartTransition(transition.mType, transition); + ActivityManager.RunningTaskInfo info = null; + if (startTask != null) { + info = new ActivityManager.RunningTaskInfo(); + startTask.fillTaskInfo(info); + } + mTransitionPlayer.requestStartTransition(transition.mType, transition, info); } catch (RemoteException e) { Slog.e(TAG, "Error requesting transition", e); transition.start(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 5f2b628e056a0..b0e67ce177118 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -19,8 +19,8 @@ package com.android.server.wm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; -import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; +import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; @@ -163,6 +163,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub try { synchronized (mGlobalLock) { Transition transition = Transition.fromBinder(transitionToken); + // In cases where transition is already provided, the "readiness lifecycle" of the + // transition is determined outside of this transaction. However, if this is a + // direct call from shell, the entire transition lifecycle is contained in the + // provided transaction and thus we can setReady immediately after apply. + boolean needsSetReady = transition == null && t != null; if (transition == null) { if (type < 0) { throw new IllegalArgumentException("Can't create transition with no type"); @@ -174,6 +179,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub t = new WindowContainerTransaction(); } applyTransaction(t, -1 /*syncId*/, transition); + if (needsSetReady) { + transition.setReady(); + } return transition; } } finally { @@ -258,14 +266,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if (transition != null) { transition.collect(wc); - if (hop.isReparent() && hop.getNewParent() != null) { - final WindowContainer parentWc = - WindowContainer.fromBinder(hop.getNewParent()); - if (parentWc == null) { - Slog.e(TAG, "Can't resolve parent window from token"); - continue; + if (hop.isReparent()) { + if (wc.getParent() != null) { + // Collect the current parent. It's visibility may change as a result + // of this reparenting. + transition.collect(wc.getParent()); + } + if (hop.getNewParent() != null) { + final WindowContainer parentWc = + WindowContainer.fromBinder(hop.getNewParent()); + if (parentWc == null) { + Slog.e(TAG, "Can't resolve parent window from token"); + continue; + } + transition.collect(parentWc); } - transition.collect(parentWc); } } effects |= sanitizeAndApplyHierarchyOp(wc, hop); @@ -307,6 +322,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) { // Already calls ensureActivityConfig mService.mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS); + mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) { final PooledConsumer f = PooledLambda.obtainConsumer( ActivityRecord::ensureActivityConfiguration, diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 74248a950a383..401ace03c5546 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -77,6 +77,7 @@ public class TransitionTests extends WindowTestsBase { changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, true /* exChg */)); changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */)); + fillChangeMap(changes, newTask); // End states. closing.mVisibleRequested = false; opening.mVisibleRequested = true; @@ -141,6 +142,7 @@ public class TransitionTests extends WindowTestsBase { changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); changes.put(opening2, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); changes.put(closing, new Transition.ChangeInfo(true /* vis */, true /* exChg */)); + fillChangeMap(changes, newTask); // End states. closing.mVisibleRequested = false; opening.mVisibleRequested = true; @@ -189,6 +191,8 @@ public class TransitionTests extends WindowTestsBase { changes.put(tda, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); changes.put(showing, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); changes.put(showing2, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + fillChangeMap(changes, tda); + // End states. showing.mVisibleRequested = true; showing2.mVisibleRequested = true; @@ -338,4 +342,12 @@ public class TransitionTests extends WindowTestsBase { assertEquals(FLAG_SHOW_WALLPAPER, info.getChange( tasks[showWallpaperTask].mRemoteToken.toWindowContainerToken()).getFlags()); } + + /** Fill the change map with all the parents of top. Change maps are usually fully populated */ + private static void fillChangeMap(ArrayMap changes, + WindowContainer top) { + for (WindowContainer curr = top.getParent(); curr != null; curr = curr.getParent()) { + changes.put(curr, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + } + } }