Use snapshot in enter content-pip animation

When entering content-pip
- We take a snapshot for the host task within the
  ActivityStarter#startActivityInner function, this is to make sure that
  the snapshot is taken before an app switches to the placeholder content
- We retrieve the snapshot in WM Shell when animating the newly created
  content-pip task, and use that snapshot during the animation
- This works similar to the solid color content overlay when app enters
  PiP without specifying a valid source rect hint. Therefore, we
  abstract the functionality to a dedicated PipContentOverlay class

Video: http://recall/-/aaaaaabFQoRHlzixHdtY/dRSTf7prHc0krPhTKZ0kXx
Bug: 220191201
Test: enter content-pip from ApiDemos app, see also Video
Change-Id: I2420402341a910b44bd8f64e3ad518ebef41bc19
This commit is contained in:
Hongwei Wang
2022-05-18 11:14:53 -07:00
parent e55b0a1f17
commit fe5078f026
8 changed files with 229 additions and 65 deletions

View File

@@ -34,10 +34,7 @@ import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import android.view.DisplayCutout;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
import java.lang.annotation.Retention;
@@ -355,20 +352,6 @@ public class TaskInfo {
return isVisible;
}
/**
* @param isLowResolution
* @return
* @hide
*/
public TaskSnapshot getTaskSnapshot(boolean isLowResolution) {
try {
return ActivityTaskManager.getService().getTaskSnapshot(taskId, isLowResolution);
} catch (RemoteException e) {
Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e);
return null;
}
}
/** @hide */
@NonNull
@TestApi

View File

@@ -27,13 +27,11 @@ import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.app.TaskInfo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -257,7 +255,7 @@ public class PipAnimationController {
mSurfaceControlTransactionFactory;
private PipSurfaceTransactionHelper mSurfaceTransactionHelper;
private @TransitionDirection int mTransitionDirection;
protected SurfaceControl mContentOverlay;
protected PipContentOverlay mContentOverlay;
private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash,
@AnimationType int animationType,
@@ -335,43 +333,26 @@ public class PipAnimationController {
return false;
}
SurfaceControl getContentOverlay() {
return mContentOverlay;
SurfaceControl getContentOverlayLeash() {
return mContentOverlay == null ? null : mContentOverlay.mLeash;
}
PipTransitionAnimator<T> setUseContentOverlay(Context context) {
void setColorContentOverlay(Context context) {
final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
if (mContentOverlay != null) {
// remove existing content overlay if there is any.
tx.remove(mContentOverlay);
tx.apply();
mContentOverlay.detach(tx);
}
mContentOverlay = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite("PipAnimation")
.setName("PipContentOverlay")
.setColorLayer()
.build();
tx.show(mContentOverlay);
tx.setLayer(mContentOverlay, Integer.MAX_VALUE);
tx.setColor(mContentOverlay, getContentOverlayColor(context));
tx.setAlpha(mContentOverlay, 0f);
tx.reparent(mContentOverlay, mLeash);
tx.apply();
return this;
mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
mContentOverlay.attach(tx, mLeash);
}
private float[] getContentOverlayColor(Context context) {
final TypedArray ta = context.obtainStyledAttributes(new int[] {
android.R.attr.colorBackground });
try {
int colorAccent = ta.getColor(0, 0);
return new float[] {
Color.red(colorAccent) / 255f,
Color.green(colorAccent) / 255f,
Color.blue(colorAccent) / 255f };
} finally {
ta.recycle();
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
mContentOverlay.attach(tx, mLeash);
}
/**
@@ -575,7 +556,7 @@ public class PipAnimationController {
final Rect start = getStartValue();
final Rect end = getEndValue();
if (mContentOverlay != null) {
tx.setAlpha(mContentOverlay, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
mContentOverlay.onAnimationUpdate(tx, fraction);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
@@ -680,7 +661,7 @@ public class PipAnimationController {
.round(tx, leash, shouldApplyCornerRadius())
.shadow(tx, leash, shouldApplyShadowRadius());
// TODO(b/178632364): this is a work around for the black background when
// entering PiP in buttion navigation mode.
// entering PiP in button navigation mode.
if (isInPipDirection(direction)) {
tx.setWindowCrop(leash, getStartValue());
}
@@ -704,6 +685,9 @@ public class PipAnimationController {
} else {
getSurfaceTransactionHelper().crop(tx, leash, destBounds);
}
if (mContentOverlay != null) {
mContentOverlay.onAnimationEnd(tx, destBounds);
}
}
@Override

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2022 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.pip;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskSnapshot;
/**
* Represents the content overlay used during the entering PiP animation.
*/
public abstract class PipContentOverlay {
protected SurfaceControl mLeash;
/** Attaches the internal {@link #mLeash} to the given parent leash. */
public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash);
/** Detaches the internal {@link #mLeash} from its parent by removing itself. */
public void detach(SurfaceControl.Transaction tx) {
if (mLeash != null && mLeash.isValid()) {
tx.remove(mLeash);
tx.apply();
}
}
/**
* Animates the internal {@link #mLeash} by a given fraction.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
* call apply on this transaction, it should be applied on the caller side.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction);
/**
* Callback when reaches the end of animation on the internal {@link #mLeash}.
* @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
* call apply on this transaction, it should be applied on the caller side.
* @param destinationBounds {@link Rect} of the final bounds.
*/
public abstract void onAnimationEnd(SurfaceControl.Transaction atomicTx,
Rect destinationBounds);
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
private final Context mContext;
public PipColorOverlay(Context context) {
mContext = context;
mLeash = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite("PipAnimation")
.setName(PipColorOverlay.class.getSimpleName())
.setColorLayer()
.build();
}
@Override
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
tx.setColor(mLeash, getContentOverlayColor(mContext));
tx.setAlpha(mLeash, 0f);
tx.reparent(mLeash, parentLeash);
tx.apply();
}
@Override
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@Override
public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
// Do nothing. Color overlay should be fully opaque by now.
}
private float[] getContentOverlayColor(Context context) {
final TypedArray ta = context.obtainStyledAttributes(new int[] {
android.R.attr.colorBackground });
try {
int colorAccent = ta.getColor(0, 0);
return new float[] {
Color.red(colorAccent) / 255f,
Color.green(colorAccent) / 255f,
Color.blue(colorAccent) / 255f };
} finally {
ta.recycle();
}
}
}
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
public static final class PipSnapshotOverlay extends PipContentOverlay {
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
private float mTaskSnapshotScaleX;
private float mTaskSnapshotScaleY;
public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite("PipAnimation")
.setName(PipSnapshotOverlay.class.getSimpleName())
.build();
}
@Override
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
mTaskSnapshotScaleX = (float) mSnapshot.getTaskSize().x
/ mSnapshot.getHardwareBuffer().getWidth();
mTaskSnapshotScaleY = (float) mSnapshot.getTaskSize().y
/ mSnapshot.getHardwareBuffer().getHeight();
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer());
tx.setCrop(mLeash, mSourceRectHint);
tx.setScale(mLeash, mTaskSnapshotScaleX, mTaskSnapshotScaleY);
tx.reparent(mLeash, parentLeash);
tx.apply();
}
@Override
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
// Do nothing. Keep the snapshot till animation ends.
}
@Override
public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
// Work around to make sure the snapshot overlay is aligned with PiP window before
// the atomicTx is committed along with the final WindowContainerTransaction.
final SurfaceControl.Transaction nonAtomicTx = new SurfaceControl.Transaction();
final float scaleX = (float) destinationBounds.width()
/ mSnapshot.getHardwareBuffer().getWidth();
final float scaleY = (float) destinationBounds.height()
/ mSnapshot.getHardwareBuffer().getHeight();
final float scale = Math.max(scaleX, scaleY);
nonAtomicTx.setScale(mLeash, scale, scale);
nonAtomicTx.setPosition(mLeash,
-scale * mSourceRectHint.left / mTaskSnapshotScaleX,
-scale * mSourceRectHint.top / mTaskSnapshotScaleY);
nonAtomicTx.apply();
atomicTx.remove(mLeash);
}
}
}

View File

@@ -66,6 +66,7 @@ import android.view.Display;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.TaskOrganizer;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -152,8 +153,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final int direction = animator.getTransitionDirection();
final int animationType = animator.getAnimationType();
final Rect destinationBounds = animator.getDestinationBounds();
if (isInPipDirection(direction) && animator.getContentOverlay() != null) {
fadeOutAndRemoveOverlay(animator.getContentOverlay(),
if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(),
animator::clearContentOverlay, true /* withStartDelay*/);
}
if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS
@@ -186,8 +187,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
public void onPipAnimationCancel(TaskInfo taskInfo,
PipAnimationController.PipTransitionAnimator animator) {
final int direction = animator.getTransitionDirection();
if (isInPipDirection(direction) && animator.getContentOverlay() != null) {
fadeOutAndRemoveOverlay(animator.getContentOverlay(),
if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(),
animator::clearContentOverlay, true /* withStartDelay */);
}
sendOnPipTransitionCancelled(direction);
@@ -803,8 +804,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
final PipAnimationController.PipTransitionAnimator<?> animator =
mPipAnimationController.getCurrentAnimator();
if (animator != null) {
if (animator.getContentOverlay() != null) {
removeContentOverlay(animator.getContentOverlay(), animator::clearContentOverlay);
if (animator.getContentOverlayLeash() != null) {
removeContentOverlay(animator.getContentOverlayLeash(),
animator::clearContentOverlay);
}
animator.removeAllUpdateListeners();
animator.removeAllListeners();
@@ -1486,7 +1488,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
if (isInPipDirection(direction)) {
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) animator.setUseContentOverlay(mContext);
if (sourceHintRect == null) {
animator.setColorContentOverlay(mContext);
} else {
final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);
if (snapshot != null) {
// use the task snapshot during the animation, this is for
// launch-into-pip aka. content-pip use case.
animator.setSnapshotContentOverlay(snapshot, sourceHintRect);
}
}
// The destination bounds are used for the end rect of animation and the final bounds
// after animation finishes. So after the animation is started, the destination bounds
// can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout
@@ -1550,7 +1562,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback,
boolean withStartDelay) {
if (surface == null) {
if (surface == null || !surface.isValid()) {
return;
}

View File

@@ -709,7 +709,7 @@ public class PipTransition extends PipTransitionController {
if (sourceHintRect == null) {
// We use content overlay when there is no source rect hint to enter PiP use bounds
// animation.
animator.setUseContentOverlay(mContext);
animator.setColorContentOverlay(mContext);
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
startTransaction.setAlpha(leash, 0f);

View File

@@ -70,8 +70,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH
if (direction == TRANSITION_DIRECTION_REMOVE_STACK) {
return;
}
if (isInPipDirection(direction) && animator.getContentOverlay() != null) {
mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(),
if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(),
animator::clearContentOverlay, true /* withStartDelay*/);
}
onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx);
@@ -82,8 +82,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void onPipAnimationCancel(TaskInfo taskInfo,
PipAnimationController.PipTransitionAnimator animator) {
final int direction = animator.getTransitionDirection();
if (isInPipDirection(direction) && animator.getContentOverlay() != null) {
mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlay(),
if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) {
mPipOrganizer.fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(),
animator::clearContentOverlay, true /* withStartDelay */);
}
sendOnPipTransitionCancelled(animator.getTransitionDirection());

View File

@@ -19,13 +19,16 @@ package com.android.wm.shell.pip;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import android.window.TaskSnapshot;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -106,4 +109,17 @@ public class PipUtils {
}
return false;
}
/** @return {@link TaskSnapshot} for a given task id. */
@Nullable
public static TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) {
if (taskId <= 0) return null;
try {
return ActivityTaskManager.getService().getTaskSnapshot(
taskId, isLowResolution);
} catch (RemoteException e) {
Log.e(TAG, "Failed to get task snapshot, taskId=" + taskId, e);
return null;
}
}
}

View File

@@ -2114,6 +2114,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
rootTask.setWindowingMode(WINDOWING_MODE_PINNED);
// Set the launch bounds for launch-into-pip Activity on the root task.
if (r.getOptions() != null && r.getOptions().isLaunchIntoPip()) {
// Record the snapshot now, it will be later fetched for content-pip animation.
// We do this early in the process to make sure the right snapshot is used for
// entering content-pip animation.
mWindowManager.mTaskSnapshotController.recordTaskSnapshot(
task, false /* allowSnapshotHome */);
rootTask.setBounds(r.getOptions().getLaunchBounds());
}
rootTask.setDeferTaskAppear(false);