blob: 143b42a1ec96745be6016c5e3549dd546c757003 [file] [log] [blame]
/*
* Copyright (C) 2023 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.util;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.RemoteAnimationTarget.MODE_CHANGING;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
import android.graphics.Rect;
import android.util.ArrayMap;
import android.util.SparseBooleanArray;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
import java.util.function.Predicate;
/** Various utility functions for transitions. */
public class TransitionUtil {
/** @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 == TRANSIT_KEYGUARD_GOING_AWAY;
}
/** @return true if the transition was triggered by closing something vs opening something */
public static boolean isClosingType(@WindowManager.TransitionType int type) {
return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
}
/** Returns {@code true} if the transition is opening or closing mode. */
public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE
|| mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK;
}
/** Returns {@code true} if the transition has a display change. */
public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
return true;
}
}
return false;
}
/** Returns `true` if `change` is a wallpaper. */
public static boolean isWallpaper(TransitionInfo.Change change) {
return (change.getTaskInfo() == null)
&& change.hasFlags(FLAG_IS_WALLPAPER)
&& !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
}
/** Returns `true` if `change` is not an app window or wallpaper. */
public static boolean isNonApp(TransitionInfo.Change change) {
return (change.getTaskInfo() == null)
&& !change.hasFlags(FLAG_IS_WALLPAPER)
&& !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
}
/** Returns `true` if `change` is the divider. */
public static boolean isDividerBar(TransitionInfo.Change change) {
return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
}
/** Returns `true` if `change` is only re-ordering. */
public static boolean isOrderOnly(TransitionInfo.Change change) {
return change.getMode() == TRANSIT_CHANGE
&& (change.getFlags() & FLAG_MOVED_TO_TOP) != 0
&& change.getStartAbsBounds().equals(change.getEndAbsBounds())
&& (change.getLastParent() == null
|| change.getLastParent().equals(change.getParent()));
}
/**
* Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
* MUST call `test` in the same order that the changes appear in the TransitionInfo.
*/
public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> {
private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray();
@Override
public boolean test(TransitionInfo.Change change) {
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
// Children always come before parent since changes are in top-to-bottom z-order.
if ((taskInfo == null) || mChildTaskTargets.get(taskInfo.taskId)) {
// has children, so not a leaf. Skip.
return false;
}
if (taskInfo.hasParentTask()) {
mChildTaskTargets.put(taskInfo.parentTaskId, true);
}
return true;
}
}
private static int newModeToLegacyMode(int newMode) {
switch (newMode) {
case WindowManager.TRANSIT_OPEN:
case WindowManager.TRANSIT_TO_FRONT:
return MODE_OPENING;
case WindowManager.TRANSIT_CLOSE:
case WindowManager.TRANSIT_TO_BACK:
return MODE_CLOSING;
default:
return MODE_CHANGING;
}
}
/**
* Very similar to Transitions#setupAnimHierarchy but specialized for leashes.
*/
@SuppressLint("NewApi")
private static void setupLeash(@NonNull SurfaceControl leash,
@NonNull TransitionInfo.Change change, int layer,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
final boolean isOpening = TransitionUtil.isOpeningType(info.getType());
// Put animating stuff above this line and put static stuff below it.
int zSplitLine = info.getChanges().size();
// changes should be ordered top-to-bottom in z
final int mode = change.getMode();
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
t.reparent(leash, info.getRoot(rootIdx).getLeash());
final Rect absBounds =
(mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
absBounds.top - info.getRoot(rootIdx).getOffset().y);
if (isDividerBar(change)) {
if (isOpeningType(mode)) {
t.setAlpha(leash, 0.f);
}
t.setLayer(leash, Integer.MAX_VALUE);
return;
}
// Put all the OPEN/SHOW on top
if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
// Wallpaper is always at the bottom, opening wallpaper on top of closing one.
if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer);
} else {
t.setLayer(leash, -zSplitLine - layer);
}
} else if (TransitionUtil.isOpeningType(mode)) {
if (isOpening) {
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
// if transferred, it should be left visible.
t.setAlpha(leash, 0.f);
}
} else {
// put on bottom and leave it visible
t.setLayer(leash, zSplitLine - layer);
}
} else if (TransitionUtil.isClosingType(mode)) {
if (isOpening) {
// put on bottom and leave visible
t.setLayer(leash, zSplitLine - layer);
} else {
// put on top
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
}
} else { // CHANGE
t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
}
}
@SuppressLint("NewApi")
private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change,
int order, SurfaceControl.Transaction t) {
// TODO: once we can properly sync transactions across process, then get rid of this leash.
if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
// Special case for wallpaper atm. Normally these are left alone; but, a quirk of
// making leashes means we have to handle them specially.
return change.getLeash();
}
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
SurfaceControl leashSurface = new SurfaceControl.Builder()
.setName(change.getLeash().toString() + "_transition-leash")
.setContainerLayer()
// Initial the surface visible to respect the visibility of the original surface.
.setHidden(false)
.setParent(info.getRoot(rootIdx).getLeash())
.build();
// Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
t.reparent(change.getLeash(), leashSurface);
t.setAlpha(change.getLeash(), 1.0f);
t.show(change.getLeash());
t.setPosition(change.getLeash(), 0, 0);
t.setLayer(change.getLeash(), 0);
return leashSurface;
}
/**
* Creates a new RemoteAnimationTarget from the provided change info
*/
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
TransitionInfo info, SurfaceControl.Transaction t,
@Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap);
}
/**
* Creates a new RemoteAnimationTarget from the provided change info
*/
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t,
@Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
final SurfaceControl leash = createLeash(info, change, order, t);
if (leashMap != null) {
leashMap.put(change.getLeash(), leash);
}
return newTarget(change, order, leash, forceTranslucent);
}
/**
* Creates a new RemoteAnimationTarget from the provided change and leash
*/
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
SurfaceControl leash) {
return newTarget(change, order, leash, false /* forceTranslucent */);
}
/**
* Creates a new RemoteAnimationTarget from the provided change and leash
*/
public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
SurfaceControl leash, boolean forceTranslucent) {
if (isDividerBar(change)) {
return getDividerTarget(change, leash);
}
int taskId;
boolean isNotInRecents;
ActivityManager.RunningTaskInfo taskInfo;
WindowConfiguration windowConfiguration;
taskInfo = change.getTaskInfo();
if (taskInfo != null) {
taskId = taskInfo.taskId;
isNotInRecents = !taskInfo.isRunning;
windowConfiguration = taskInfo.configuration.windowConfiguration;
} else {
taskId = INVALID_TASK_ID;
isNotInRecents = true;
windowConfiguration = new WindowConfiguration();
}
Rect localBounds = new Rect(change.getEndAbsBounds());
localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
RemoteAnimationTarget target = new RemoteAnimationTarget(
taskId,
newModeToLegacyMode(change.getMode()),
// TODO: once we can properly sync transactions across process,
// then get rid of this leash.
leash,
forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
null,
// TODO(shell-transitions): we need to send content insets? evaluate how its used.
new Rect(0, 0, 0, 0),
order,
null,
localBounds,
new Rect(change.getEndAbsBounds()),
windowConfiguration,
isNotInRecents,
null,
new Rect(change.getStartAbsBounds()),
taskInfo,
change.getAllowEnterPip(),
INVALID_WINDOW_TYPE
);
target.setWillShowImeOnTarget(
(change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
target.setRotationChange(change.getEndRotation() - change.getStartRotation());
target.backgroundColor = change.getBackgroundColor();
return target;
}
private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
SurfaceControl leash) {
return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
leash, false /* isTranslucent */, null /* clipRect */,
null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
TYPE_DOCK_DIVIDER);
}
/**
* Finds the "correct" root idx for a change. The change's end display is prioritized, then
* the start display. If there is no display, it will fallback on the 0th root in the
* transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
*/
public static int rootIndexFor(@NonNull TransitionInfo.Change change,
@NonNull TransitionInfo info) {
int rootIdx = info.findRootIndex(change.getEndDisplayId());
if (rootIdx >= 0) return rootIdx;
rootIdx = info.findRootIndex(change.getStartDisplayId());
if (rootIdx >= 0) return rootIdx;
return 0;
}
/**
* Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}.
* @see #rootIndexFor(TransitionInfo.Change, TransitionInfo)
*/
@NonNull
public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change,
@NonNull TransitionInfo info) {
return info.getRoot(rootIndexFor(change, info));
}
}