blob: 8d4a38442ce5d6d80eb4acca969d33d1806215e0 [file] [log] [blame]
/*
* 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.tv;
import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Controller managing the PiP's position.
* Manages debouncing of PiP movements and scheduling of unstashing.
*/
public class TvPipBoundsController {
private static final String TAG = "TvPipBoundsController";
/**
* Time the calculated PiP position needs to be stable before PiP is moved there,
* to avoid erratic movement.
* Some changes will cause the PiP to be repositioned immediately, such as changes to
* unrestricted keep clear areas.
*/
@VisibleForTesting
static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L;
private final Context mContext;
private final Supplier<Long> mClock;
private final Handler mMainHandler;
private final TvPipBoundsState mTvPipBoundsState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
@Nullable
private PipBoundsListener mListener;
private int mResizeAnimationDuration;
private int mStashDurationMs;
private Rect mCurrentPlacementBounds;
private Rect mPipTargetBounds;
private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement;
private boolean mPendingStash;
private Placement mPendingPlacement;
private int mPendingPlacementAnimationDuration;
private Runnable mUnstashRunnable;
public TvPipBoundsController(
Context context,
Supplier<Long> clock,
Handler mainHandler,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
mContext = context;
mClock = clock;
mMainHandler = mainHandler;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
loadConfigurations();
}
private void loadConfigurations() {
final Resources res = mContext.getResources();
mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration);
}
void setListener(PipBoundsListener listener) {
mListener = listener;
}
/**
* Update the PiP bounds based on the state of the PiP, decors, and keep clear areas.
* Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid
* keep clear areas, but waits for a new position to stay uncontested for
* {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it.
* Temporary decor changes are applied immediately.
*
* @param stayAtAnchorPosition If true, PiP will be placed at the anchor position
* @param disallowStashing If true, PiP will not be placed off-screen in a stashed position
* @param animationDuration Duration of the animation to the new position
* @param immediate If true, PiP will move immediately to avoid keep clear areas
*/
@VisibleForTesting
void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing,
int animationDuration, boolean immediate) {
final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement();
final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType();
mTvPipBoundsState.setStashed(stashType);
if (stayAtAnchorPosition) {
cancelScheduledPlacement();
applyPlacementBounds(placement.getAnchorBounds(), animationDuration);
} else if (disallowStashing) {
cancelScheduledPlacement();
applyPlacementBounds(placement.getUnstashedBounds(), animationDuration);
} else if (immediate) {
boolean shouldStash = mUnstashRunnable != null || placement.getTriggerStash();
cancelScheduledPlacement();
applyPlacement(placement, shouldStash, animationDuration);
} else {
if (mCurrentPlacementBounds != null) {
applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
}
schedulePinnedStackPlacement(placement, animationDuration);
}
}
private void schedulePinnedStackPlacement(@NonNull final Placement placement,
int animationDuration) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: schedulePinnedStackPlacement() - pip bounds: %s",
TAG, placement.getBounds().toShortString());
if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(),
placement.getBounds())) {
mPendingStash = mPendingStash || placement.getTriggerStash();
return;
}
mPendingStash = placement.getStashType() != STASH_TYPE_NONE
&& (mPendingStash || placement.getTriggerStash());
mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
mPendingPlacement = placement;
mPendingPlacementAnimationDuration = animationDuration;
mMainHandler.postAtTime(mApplyPendingPlacementRunnable,
mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS);
}
private void scheduleUnstashIfNeeded(final Placement placement) {
if (mUnstashRunnable != null) {
mMainHandler.removeCallbacks(mUnstashRunnable);
mUnstashRunnable = null;
}
if (placement.getUnstashDestinationBounds() != null) {
mUnstashRunnable = () -> {
applyPlacementBounds(placement.getUnstashDestinationBounds(),
mResizeAnimationDuration);
mUnstashRunnable = null;
};
mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs);
}
}
private void applyPendingPlacement() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: applyPendingPlacement()", TAG);
if (mPendingPlacement != null) {
applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration);
mPendingStash = false;
mPendingPlacement = null;
}
}
private void applyPlacement(@NonNull final Placement placement, boolean shouldStash,
int animationDuration) {
if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) {
scheduleUnstashIfNeeded(placement);
}
Rect bounds =
mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds();
applyPlacementBounds(bounds, animationDuration);
}
void reset() {
mCurrentPlacementBounds = null;
mPipTargetBounds = null;
cancelScheduledPlacement();
}
private void cancelScheduledPlacement() {
mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
mPendingPlacement = null;
if (mUnstashRunnable != null) {
mMainHandler.removeCallbacks(mUnstashRunnable);
mUnstashRunnable = null;
}
}
private void applyPlacementBounds(Rect bounds, int animationDuration) {
if (bounds == null) {
return;
}
mCurrentPlacementBounds = bounds;
Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds);
movePipTo(adjustedBounds, animationDuration);
}
/** Animates the PiP to the given bounds with the given animation duration. */
private void movePipTo(Rect bounds, int animationDuration) {
if (Objects.equals(mPipTargetBounds, bounds)) {
return;
}
mPipTargetBounds = bounds;
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
if (mListener != null) {
mListener.onPipTargetBoundsChange(bounds, animationDuration);
}
}
/**
* Interface being notified of changes to the PiP bounds as calculated by
* @link TvPipBoundsController}.
*/
public interface PipBoundsListener {
/**
* Called when the calculated PiP bounds are changing.
*
* @param newTargetBounds The new bounds of the PiP.
* @param animationDuration The animation duration for the PiP movement.
*/
void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration);
}
}