Basic support for split-screen in new transition system

Sends the "trigger" taskinfo associated with a transition when
applicable. This can be used by the transitionplayer to make
decisions about how to start and play transitions.

Additionally, TaskInfo is provided in each Change object if
they are tasks. This provides all the information needed to
choose how to handle split.

Shell's transitionplayer makes use of this to add support for
"handlers" to deal with specific transition requests. Currently
a single handler is added to process split-screen transitions.

Bug: 169035082
Test: Use split-screen (enter, exit via swipe up/down,
      exit via dismiss top)
Change-Id: I53a03dd85cb260505e66d5d87a34afa4ab1f8cc8
This commit is contained in:
Evan Rosky
2020-11-10 17:08:27 -08:00
parent a37c840348
commit 68fbb47250
22 changed files with 738 additions and 110 deletions

View File

@@ -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 {}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);
});
}

View File

@@ -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<TransitionHandler> mHandlers = new ArrayList<>();
private static final class ActiveTransition {
ArrayList<Animator> mAnimations = null;
TransitionHandler mFirstHandler = null;
}
/** Keeps track of currently tracked transitions and all the animations associated with each */
private final ArrayMap<IBinder, ArrayList<Animator>> mActiveTransitions = new ArrayMap<>();
private final ArrayMap<IBinder, ActiveTransition> 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);
});
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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<Animator> 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;
}
}

View File

@@ -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<ActivityManager.RunningTaskInfo> 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<ActivityManager.RunningTaskInfo> primaryChildren =
mTaskOrganizer.getChildTasks(tiles.mPrimary.token, null /* activityTypes */);
taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */);
List<ActivityManager.RunningTaskInfo> 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<ActivityManager.RunningTaskInfo> 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 */);
}
/**

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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<WindowContainer, Transition.ChangeInfo> changes,
WindowContainer top) {
for (WindowContainer curr = top.getParent(); curr != null; curr = curr.getParent()) {
changes.put(curr, new Transition.ChangeInfo(true /* vis */, false /* exChg */));
}
}
}