Fix entering PiP transition in button navigation mode

Included in this CL
- when there is no source rect hint, use a content overlay during the transition
- otherwise, take account the display cutout to offset/inset the source
  rect hint

Test with following variants when entering PiP in button nav
- from 0 / 90 / 270 rotation
- with or without source rect hint
- display cutout mode set to default or shortEdge

Video: http://recall/-/aaaaaabFQoRHlzixHdtY/cxk4vy8VenQPSv83vHEs22
Bug: 191310680
Test: manual, see the test cases listed and video above
Change-Id: Ie54a54de6e55397e25024373ea4e2855fde2d9f7
Merged-In: Ie54a54de6e55397e25024373ea4e2855fde2d9f7
This commit is contained in:
Hongwei Wang 2021-07-15 15:30:15 -07:00
parent 5a59072018
commit 33c3fcc671
5 changed files with 130 additions and 26 deletions
libs/WindowManager/Shell/src/com/android/wm/shell/pip
services/core/java/com/android/server/wm

@ -26,10 +26,14 @@ import android.animation.RectEvaluator;
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 com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@ -253,6 +257,7 @@ public class PipAnimationController {
mSurfaceControlTransactionFactory;
private PipSurfaceTransactionHelper mSurfaceTransactionHelper;
private @TransitionDirection int mTransitionDirection;
protected SurfaceControl mContentOverlay;
private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash,
@AnimationType int animationType, Rect destinationBounds, T baseValue, T startValue,
@ -331,6 +336,53 @@ public class PipAnimationController {
return false;
}
SurfaceControl getContentOverlay() {
return mContentOverlay;
}
PipTransitionAnimator<T> setUseContentOverlay(Context context) {
final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
if (mContentOverlay != null) {
// remove existing content overlay if there is any.
tx.remove(mContentOverlay);
tx.apply();
}
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;
}
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();
}
}
/**
* Clears the {@link #mContentOverlay}, this should be done after the content overlay is
* faded out, such as in {@link PipTaskOrganizer#fadeOutAndRemoveOverlay}
*/
void clearContentOverlay() {
mContentOverlay = null;
}
@VisibleForTesting
@TransitionDirection public int getTransitionDirection() {
return mTransitionDirection;
@ -517,6 +569,9 @@ public class PipAnimationController {
final Rect base = getBaseValue();
final Rect start = getStartValue();
final Rect end = getEndValue();
if (mContentOverlay != null) {
tx.setAlpha(mContentOverlay, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
// switching between PiP and fullscreen.

@ -107,6 +107,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
*/
private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 1000;
/**
* The fixed start delay in ms when fading out the content overlay from bounds animation.
* This is to overcome the flicker caused by configuration change when rotating from landscape
* to portrait PiP in button navigation mode.
*/
private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500;
// Not a complete set of states but serves what we want right now.
private enum State {
UNDEFINED(0),
@ -176,6 +183,10 @@ 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(),
animator::clearContentOverlay, true /* withStartDelay*/);
}
if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS
&& direction == TRANSITION_DIRECTION_TO_PIP) {
// Notify the display to continue the deferred orientation change.
@ -199,17 +210,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
finishResize(tx, destinationBounds, direction, animationType);
sendOnPipTransitionFinished(direction);
}
if (direction == TRANSITION_DIRECTION_TO_PIP) {
// TODO (b//169221267): Add jank listener for transactions without buffer updates.
//InteractionJankMonitor.getInstance().end(
// InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
}
}
@Override
public void onPipAnimationCancel(TaskInfo taskInfo,
PipAnimationController.PipTransitionAnimator animator) {
sendOnPipTransitionCancelled(animator.getTransitionDirection());
final int direction = animator.getTransitionDirection();
if (isInPipDirection(direction) && animator.getContentOverlay() != null) {
fadeOutAndRemoveOverlay(animator.getContentOverlay(),
animator::clearContentOverlay, true /* withStartDelay */);
}
sendOnPipTransitionCancelled(direction);
}
};
@ -640,7 +651,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// Remove the swipe to home overlay
if (swipeToHomeOverlay != null) {
fadeOutAndRemoveOverlay(swipeToHomeOverlay);
fadeOutAndRemoveOverlay(swipeToHomeOverlay,
null /* callback */, false /* withStartDelay */);
}
}, tx);
mInSwipePipToHomeTransition = false;
@ -723,9 +735,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY);
}
final PipAnimationController.PipTransitionAnimator animator =
final PipAnimationController.PipTransitionAnimator<?> animator =
mPipAnimationController.getCurrentAnimator();
if (animator != null) {
if (animator.getContentOverlay() != null) {
removeContentOverlay(animator.getContentOverlay(), animator::clearContentOverlay);
}
animator.removeAllUpdateListeners();
animator.removeAllListeners();
animator.cancel();
@ -1196,7 +1211,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
snapshotDest);
// Start animation to fade out the snapshot.
fadeOutAndRemoveOverlay(snapshotSurface);
fadeOutAndRemoveOverlay(snapshotSurface,
null /* callback */, false /* withStartDelay */);
});
} else {
applyFinishBoundsResize(wct, direction);
@ -1287,15 +1303,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
animator.setTransitionDirection(direction)
.setPipAnimationCallback(mPipAnimationCallback)
.setPipTransactionHandler(mPipTransactionHandler)
.setDuration(durationMs)
.start();
if (rotationDelta != Surface.ROTATION_0 && direction == TRANSITION_DIRECTION_TO_PIP) {
.setDuration(durationMs);
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);
// 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
// without affecting the animation.
animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds());
if (rotationDelta != Surface.ROTATION_0) {
animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds());
}
}
animator.start();
return animator;
}
@ -1308,6 +1329,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
// Transform the destination bounds to current display coordinates.
rotateBounds(outDestinationBounds, displayBounds, mNextRotation, mCurrentRotation);
// When entering PiP (from button navigation mode), adjust the source rect hint by
// display cutout if applicable.
if (sourceHintRect != null && mTaskInfo.displayCutoutInsets != null) {
if (rotationDelta == Surface.ROTATION_270) {
sourceHintRect.offset(mTaskInfo.displayCutoutInsets.left,
mTaskInfo.displayCutoutInsets.top);
}
}
} else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) {
final Rect rotatedDestinationBounds = new Rect(outDestinationBounds);
rotateBounds(rotatedDestinationBounds, mPipBoundsState.getDisplayBounds(),
@ -1346,7 +1375,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
/**
* Fades out and removes an overlay surface.
*/
private void fadeOutAndRemoveOverlay(SurfaceControl surface) {
private void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback,
boolean withStartDelay) {
if (surface == null) {
return;
}
@ -1363,15 +1393,21 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
tx.remove(surface);
tx.apply();
removeContentOverlay(surface, callback);
}
});
animator.setStartDelay(withStartDelay ? CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
animator.start();
}
private void removeContentOverlay(SurfaceControl surface, Runnable callback) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
tx.remove(surface);
tx.apply();
if (callback != null) callback.run();
}
/**
* Dumps internal states.
*/

@ -1884,7 +1884,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
forAllWindows(w -> {
w.seamlesslyRotateIfAllowed(transaction, oldRotation, rotation, rotateSeamlessly);
}, true /* traverseTopToBottom */);
mPinnedTaskController.startSeamlessRotationIfNeeded(transaction);
mPinnedTaskController.startSeamlessRotationIfNeeded(transaction, oldRotation, rotation);
}
mWmService.mDisplayManagerInternal.performTraversal(transaction);

@ -27,13 +27,16 @@ import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.pm.ParceledListSlice;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.util.RotationUtils;
import android.util.Slog;
import android.view.IPinnedTaskListener;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
@ -237,7 +240,8 @@ class PinnedTaskController {
* rotation of display. The final surface matrix will be replaced by PiPTaskOrganizer after it
* receives the callback of fixed rotation completion.
*/
void startSeamlessRotationIfNeeded(SurfaceControl.Transaction t) {
void startSeamlessRotationIfNeeded(SurfaceControl.Transaction t,
int oldRotation, int newRotation) {
final Rect bounds = mDestRotatedBounds;
final PictureInPictureSurfaceTransaction pipTx = mPipTransaction;
if (bounds == null && pipTx == null) {
@ -280,6 +284,16 @@ class PinnedTaskController {
? params.getSourceRectHint()
: null;
Slog.i(TAG, "Seamless rotation PiP bounds=" + bounds + " hintRect=" + sourceHintRect);
final int rotationDelta = RotationUtils.deltaRotation(oldRotation, newRotation);
// Adjust for display cutout if applicable.
if (sourceHintRect != null && rotationDelta == Surface.ROTATION_270) {
if (pinnedTask.getDisplayCutoutInsets() != null) {
final int rotationBackDelta = RotationUtils.deltaRotation(newRotation, oldRotation);
final Rect displayCutoutInsets = RotationUtils.rotateInsets(
Insets.of(pinnedTask.getDisplayCutoutInsets()), rotationBackDelta).toRect();
sourceHintRect.offset(displayCutoutInsets.left, displayCutoutInsets.top);
}
}
final Rect contentBounds = sourceHintRect != null && areaBounds.contains(sourceHintRect)
? sourceHintRect : areaBounds;
final int w = contentBounds.width();

@ -4107,7 +4107,7 @@ class Task extends WindowContainer<WindowContainer> {
info.positionInParent = getRelativePosition();
info.pictureInPictureParams = getPictureInPictureParams(top);
info.displayCutoutInsets = getDisplayCutoutInsets(top);
info.displayCutoutInsets = top != null ? top.getDisplayCutoutInsets() : null;
info.topActivityInfo = mReuseActivitiesReport.top != null
? mReuseActivitiesReport.top.info
: null;
@ -4142,16 +4142,15 @@ class Task extends WindowContainer<WindowContainer> {
? null : new PictureInPictureParams(topVisibleActivity.pictureInPictureArgs);
}
private Rect getDisplayCutoutInsets(Task top) {
if (top == null || top.mDisplayContent == null
|| top.getDisplayInfo().displayCutout == null) return null;
final WindowState w = top.getTopVisibleAppMainWindow();
Rect getDisplayCutoutInsets() {
if (mDisplayContent == null || getDisplayInfo().displayCutout == null) return null;
final WindowState w = getTopVisibleAppMainWindow();
final int displayCutoutMode = w == null
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
: w.getAttrs().layoutInDisplayCutoutMode;
return (displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|| displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
? null : top.getDisplayInfo().displayCutout.getSafeInsets();
? null : getDisplayInfo().displayCutout.getSafeInsets();
}
/**