/* * 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.common.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 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 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); } }