blob: 836485a46e365e1ae153e0210ef6669c337333b9 [file] [log] [blame]
/*
* 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.systemui.pip;
import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
import static com.android.systemui.pip.PipAnimationController.DURATION_DEFAULT_MS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.ITaskOrganizerController;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.DisplayInfo;
import android.view.ITaskOrganizer;
import android.view.IWindowContainer;
import android.view.SurfaceControl;
import android.view.WindowContainerTransaction;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Manages PiP tasks such as resize and offset.
*
* This class listens on {@link ITaskOrganizer} callbacks for windowing mode change
* both to and from PiP and issues corresponding animation if applicable.
* Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running
* and files a final {@link WindowContainerTransaction} at the end of the transition.
*
* This class is also responsible for general resize/offset PiP operations within SysUI component,
* see also {@link com.android.systemui.pip.phone.PipMotionHelper}.
*/
public class PipTaskOrganizer extends ITaskOrganizer.Stub {
private static final String TAG = PipTaskOrganizer.class.getSimpleName();
private final Handler mMainHandler;
private final ITaskOrganizerController mTaskOrganizerController;
private final PipBoundsHandler mPipBoundsHandler;
private final PipAnimationController mPipAnimationController;
private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
private final Rect mDisplayBounds = new Rect();
private final Rect mLastReportedBounds = new Rect();
private final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
new PipAnimationController.PipAnimationCallback() {
@Override
public void onPipAnimationStart(IWindowContainer wc,
PipAnimationController.PipTransitionAnimator animator) {
mMainHandler.post(() -> {
for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
callback.onPipTransitionStarted();
}
});
}
@Override
public void onPipAnimationEnd(IWindowContainer wc, SurfaceControl.Transaction tx,
PipAnimationController.PipTransitionAnimator animator) {
mMainHandler.post(() -> {
for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
callback.onPipTransitionFinished();
}
});
final Rect destinationBounds = animator.getDestinationBounds();
finishResizeInternal(destinationBounds, wc, tx, animator.shouldScheduleFinishPip());
}
@Override
public void onPipAnimationCancel(IWindowContainer wc,
PipAnimationController.PipTransitionAnimator animator) {
mMainHandler.post(() -> {
for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
callback.onPipTransitionCanceled();
}
});
}
};
private ActivityManager.RunningTaskInfo mTaskInfo;
private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS;
public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler) {
mMainHandler = new Handler(Looper.getMainLooper());
mTaskOrganizerController = ActivityTaskManager.getTaskOrganizerController();
mPipBoundsHandler = boundsHandler;
mPipAnimationController = new PipAnimationController(context);
}
/**
* Offset the PiP window, animate if the given duration is not {@link #DURATION_NONE}
*/
public void offsetPinnedStack(Rect originalBounds, int xOffset, int yOffset, int durationMs) {
if (mTaskInfo == null) {
Log.w(TAG, "mTaskInfo is not set");
return;
}
final Rect destinationBounds = new Rect(originalBounds);
destinationBounds.offset(xOffset, yOffset);
animateResizePipInternal(mTaskInfo.token, false /* scheduleFinishPip*/,
originalBounds, destinationBounds, durationMs);
}
/**
* Registers {@link PipTransitionCallback} to receive transition callbacks.
*/
public void registerPipTransitionCallback(PipTransitionCallback callback) {
mPipTransitionCallbacks.add(callback);
}
/**
* Sets the preferred animation type for one time.
* This is typically used to set the animation type to {@link #ANIM_TYPE_ALPHA}.
*/
public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
mOneShotAnimationType = animationType;
}
/**
* Updates the display dimension with given {@link DisplayInfo}
*/
public void onDisplayInfoChanged(DisplayInfo displayInfo) {
final Rect newDisplayBounds = new Rect(0, 0,
displayInfo.logicalWidth, displayInfo.logicalHeight);
if (!mDisplayBounds.equals(newDisplayBounds)) {
// Updates the exiting PiP animation in case the screen rotation changes in the middle.
// It's a legit case that PiP window is in portrait mode on home screen and
// the application requests landscape onces back to fullscreen mode.
final PipAnimationController.PipTransitionAnimator animator =
mPipAnimationController.getCurrentAnimator();
if (animator != null
&& animator.getAnimationType() == ANIM_TYPE_BOUNDS
&& animator.getDestinationBounds().equals(mDisplayBounds)) {
animator.updateEndValue(newDisplayBounds);
animator.setDestinationBounds(newDisplayBounds);
}
}
mDisplayBounds.set(newDisplayBounds);
}
/**
* Callback to issue the final {@link WindowContainerTransaction} on end of movements.
* @param destinationBounds the final bounds.
*/
public void onMotionMovementEnd(Rect destinationBounds) {
try {
mLastReportedBounds.set(destinationBounds);
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mTaskInfo.token, destinationBounds);
mTaskOrganizerController.applyContainerTransaction(wct, null /* ITaskOrganizer */);
} catch (RemoteException e) {
Log.w(TAG, "Failed to apply window container transaction", e);
}
}
@Override
public void taskAppeared(ActivityManager.RunningTaskInfo info) {
Objects.requireNonNull(info, "Requires RunningTaskInfo");
final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
getAspectRatioOrDefault(info.pictureInPictureParams), null /* bounds */);
Objects.requireNonNull(destinationBounds, "Missing destination bounds");
mTaskInfo = info;
if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
animateResizePipInternal(mTaskInfo.token, true /* scheduleFinishPip */,
currentBounds, destinationBounds, DURATION_DEFAULT_MS);
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
mMainHandler.post(() -> mPipAnimationController
.getAnimator(mTaskInfo.token, true /* scheduleFinishPip */,
destinationBounds, 0f, 1f)
.setPipAnimationCallback(mPipAnimationCallback)
.setDuration(DURATION_DEFAULT_MS)
.start());
mOneShotAnimationType = ANIM_TYPE_BOUNDS;
} else {
throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType);
}
}
@Override
public void taskVanished(IWindowContainer token) {
Objects.requireNonNull(token, "Requires valid IWindowContainer");
if (token.asBinder() != mTaskInfo.token.asBinder()) {
Log.wtf(TAG, "Unrecognized token: " + token);
return;
}
animateResizePipInternal(token, false /* scheduleFinishPip */,
mLastReportedBounds, mDisplayBounds, DURATION_DEFAULT_MS);
}
@Override
public void transactionReady(int id, SurfaceControl.Transaction t) {
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
final PictureInPictureParams newParams = info.pictureInPictureParams;
if (!shouldUpdateDestinationBounds(newParams)) {
Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams);
return;
}
final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds(
getAspectRatioOrDefault(newParams), null /* bounds */);
Objects.requireNonNull(destinationBounds, "Missing destination bounds");
animateResizePip(destinationBounds, DURATION_DEFAULT_MS);
}
/**
* @return {@code true} if the aspect ratio is changed since no other parameters within
* {@link PictureInPictureParams} would affect the bounds.
*/
private boolean shouldUpdateDestinationBounds(PictureInPictureParams params) {
if (params == null || mTaskInfo.pictureInPictureParams == null) {
return params != mTaskInfo.pictureInPictureParams;
}
return !Objects.equals(mTaskInfo.pictureInPictureParams.getAspectRatioRational(),
params.getAspectRatioRational());
}
/**
* Directly perform manipulation/resize on the leash. This will not perform any
* {@link WindowContainerTransaction} until {@link #finishResize} is called.
*/
public void resizePip(Rect destinationBounds) {
Objects.requireNonNull(mTaskInfo, "Requires valid IWindowContainer");
resizePipInternal(mTaskInfo.token, destinationBounds);
}
private void resizePipInternal(IWindowContainer wc,
Rect destinationBounds) {
Objects.requireNonNull(mTaskInfo, "Requires valid IWindowContainer");
try {
// Could happen when dismissPip
if (wc == null || wc.getLeash() == null) {
Log.w(TAG, "Abort animation, invalid leash");
return;
}
final SurfaceControl leash = wc.getLeash();
new SurfaceControl.Transaction()
.setPosition(leash, destinationBounds.left, destinationBounds.top)
.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
.apply();
} catch (RemoteException e) {
Log.w(TAG, "Abort animation, invalid window container", e);
} catch (Exception e) {
Log.e(TAG, "Should not reach here, terrible thing happened", e);
}
}
/**
* Finish a intermediate resize operation. This is expected to be called after
* {@link #resizePip}.
*/
public void finishResize(Rect destinationBounds) {
try {
final IWindowContainer wc = mTaskInfo.token;
SurfaceControl.Transaction tx = new SurfaceControl.Transaction()
.setPosition(wc.getLeash(), destinationBounds.left,
destinationBounds.top)
.setWindowCrop(wc.getLeash(), destinationBounds.width(),
destinationBounds.height());
finishResizeInternal(destinationBounds, wc, tx, false);
} catch (RemoteException e) {
Log.e(TAG, "Failed to obtain leash");
}
}
private void finishResizeInternal(Rect destinationBounds, IWindowContainer wc,
SurfaceControl.Transaction tx, boolean shouldScheduleFinishPip) {
mLastReportedBounds.set(destinationBounds);
try {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (shouldScheduleFinishPip) {
wct.scheduleFinishEnterPip(wc, destinationBounds);
} else {
wct.setBounds(wc, destinationBounds);
}
wct.setBoundsChangeTransaction(mTaskInfo.token, tx);
mTaskOrganizerController.applyContainerTransaction(wct, null /* ITaskOrganizer */);
} catch (RemoteException e) {
Log.e(TAG, "Failed to apply container transaction", e);
}
}
/**
* Animates resizing of the pinned stack given the duration.
*/
public void animateResizePip(Rect destinationBounds, int durationMs) {
Objects.requireNonNull(mTaskInfo, "Requires valid IWindowContainer");
animateResizePipInternal(mTaskInfo.token, false, mLastReportedBounds,
destinationBounds, durationMs);
}
private void animateResizePipInternal(IWindowContainer wc, boolean scheduleFinishPip,
Rect currentBounds, Rect destinationBounds, int durationMs) {
try {
// Could happen when dismissPip
if (wc == null || wc.getLeash() == null) {
Log.w(TAG, "Abort animation, invalid leash");
return;
}
final SurfaceControl leash = wc.getLeash();
mMainHandler.post(() -> mPipAnimationController
.getAnimator(wc, scheduleFinishPip, currentBounds, destinationBounds)
.setPipAnimationCallback(mPipAnimationCallback)
.setDuration(durationMs)
.start());
} catch (RemoteException e) {
Log.w(TAG, "Abort animation, invalid window container", e);
} catch (Exception e) {
Log.e(TAG, "Should not reach here, terrible thing happened", e);
}
}
private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) {
return params == null
? mPipBoundsHandler.getDefaultAspectRatio()
: params.getAspectRatio();
}
/**
* Callback interface for PiP transitions (both from and to PiP mode)
*/
public interface PipTransitionCallback {
/**
* Callback when the pip transition is started.
*/
void onPipTransitionStarted();
/**
* Callback when the pip transition is finished.
*/
void onPipTransitionFinished();
/**
* Callback when the pip transition is cancelled.
*/
void onPipTransitionCanceled();
}
}