1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip.tv; 18 19 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Rect; 26 import android.os.Handler; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.protolog.common.ProtoLog; 30 import com.android.wm.shell.R; 31 import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; 32 import com.android.wm.shell.protolog.ShellProtoLogGroup; 33 34 import java.util.Objects; 35 import java.util.function.Supplier; 36 37 /** 38 * Controller managing the PiP's position. 39 * Manages debouncing of PiP movements and scheduling of unstashing. 40 */ 41 public class TvPipBoundsController { 42 private static final String TAG = "TvPipBoundsController"; 43 44 /** 45 * Time the calculated PiP position needs to be stable before PiP is moved there, 46 * to avoid erratic movement. 47 * Some changes will cause the PiP to be repositioned immediately, such as changes to 48 * unrestricted keep clear areas. 49 */ 50 @VisibleForTesting 51 static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L; 52 53 private final Context mContext; 54 private final Supplier<Long> mClock; 55 private final Handler mMainHandler; 56 private final TvPipBoundsState mTvPipBoundsState; 57 private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; 58 59 @Nullable 60 private PipBoundsListener mListener; 61 62 private int mResizeAnimationDuration; 63 private int mStashDurationMs; 64 private Rect mCurrentPlacementBounds; 65 private Rect mPipTargetBounds; 66 67 private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement; 68 private boolean mPendingStash; 69 private Placement mPendingPlacement; 70 private int mPendingPlacementAnimationDuration; 71 private Runnable mUnstashRunnable; 72 TvPipBoundsController( Context context, Supplier<Long> clock, Handler mainHandler, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm)73 public TvPipBoundsController( 74 Context context, 75 Supplier<Long> clock, 76 Handler mainHandler, 77 TvPipBoundsState tvPipBoundsState, 78 TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { 79 mContext = context; 80 mClock = clock; 81 mMainHandler = mainHandler; 82 mTvPipBoundsState = tvPipBoundsState; 83 mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; 84 85 loadConfigurations(); 86 } 87 loadConfigurations()88 private void loadConfigurations() { 89 final Resources res = mContext.getResources(); 90 mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); 91 mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration); 92 } 93 setListener(PipBoundsListener listener)94 void setListener(PipBoundsListener listener) { 95 mListener = listener; 96 } 97 98 /** 99 * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas. 100 * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid 101 * keep clear areas, but waits for a new position to stay uncontested for 102 * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it. 103 * Temporary decor changes are applied immediately. 104 * 105 * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position 106 * @param disallowStashing If true, PiP will not be placed off-screen in a stashed position 107 * @param animationDuration Duration of the animation to the new position 108 * @param immediate If true, PiP will move immediately to avoid keep clear areas 109 */ 110 @VisibleForTesting recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, int animationDuration, boolean immediate)111 void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, 112 int animationDuration, boolean immediate) { 113 final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement(); 114 115 final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType(); 116 mTvPipBoundsState.setStashed(stashType); 117 if (stayAtAnchorPosition) { 118 cancelScheduledPlacement(); 119 applyPlacementBounds(placement.getAnchorBounds(), animationDuration); 120 } else if (disallowStashing) { 121 cancelScheduledPlacement(); 122 applyPlacementBounds(placement.getUnstashedBounds(), animationDuration); 123 } else if (immediate) { 124 boolean shouldStash = mUnstashRunnable != null || placement.getTriggerStash(); 125 cancelScheduledPlacement(); 126 applyPlacement(placement, shouldStash, animationDuration); 127 } else { 128 if (mCurrentPlacementBounds != null) { 129 applyPlacementBounds(mCurrentPlacementBounds, animationDuration); 130 } 131 schedulePinnedStackPlacement(placement, animationDuration); 132 } 133 } 134 schedulePinnedStackPlacement(@onNull final Placement placement, int animationDuration)135 private void schedulePinnedStackPlacement(@NonNull final Placement placement, 136 int animationDuration) { 137 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 138 "%s: schedulePinnedStackPlacement() - pip bounds: %s", 139 TAG, placement.getBounds().toShortString()); 140 141 if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(), 142 placement.getBounds())) { 143 mPendingStash = mPendingStash || placement.getTriggerStash(); 144 return; 145 } 146 147 mPendingStash = placement.getStashType() != STASH_TYPE_NONE 148 && (mPendingStash || placement.getTriggerStash()); 149 150 mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); 151 mPendingPlacement = placement; 152 mPendingPlacementAnimationDuration = animationDuration; 153 mMainHandler.postAtTime(mApplyPendingPlacementRunnable, 154 mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS); 155 } 156 scheduleUnstashIfNeeded(final Placement placement)157 private void scheduleUnstashIfNeeded(final Placement placement) { 158 if (mUnstashRunnable != null) { 159 mMainHandler.removeCallbacks(mUnstashRunnable); 160 mUnstashRunnable = null; 161 } 162 if (placement.getUnstashDestinationBounds() != null) { 163 mUnstashRunnable = () -> { 164 applyPlacementBounds(placement.getUnstashDestinationBounds(), 165 mResizeAnimationDuration); 166 mUnstashRunnable = null; 167 }; 168 mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs); 169 } 170 } 171 applyPendingPlacement()172 private void applyPendingPlacement() { 173 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 174 "%s: applyPendingPlacement()", TAG); 175 if (mPendingPlacement != null) { 176 applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration); 177 mPendingStash = false; 178 mPendingPlacement = null; 179 } 180 } 181 applyPlacement(@onNull final Placement placement, boolean shouldStash, int animationDuration)182 private void applyPlacement(@NonNull final Placement placement, boolean shouldStash, 183 int animationDuration) { 184 if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) { 185 scheduleUnstashIfNeeded(placement); 186 } 187 188 Rect bounds = 189 mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds(); 190 applyPlacementBounds(bounds, animationDuration); 191 } 192 reset()193 void reset() { 194 mCurrentPlacementBounds = null; 195 mPipTargetBounds = null; 196 cancelScheduledPlacement(); 197 } 198 cancelScheduledPlacement()199 private void cancelScheduledPlacement() { 200 mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); 201 mPendingPlacement = null; 202 203 if (mUnstashRunnable != null) { 204 mMainHandler.removeCallbacks(mUnstashRunnable); 205 mUnstashRunnable = null; 206 } 207 } 208 applyPlacementBounds(Rect bounds, int animationDuration)209 private void applyPlacementBounds(Rect bounds, int animationDuration) { 210 if (bounds == null) { 211 return; 212 } 213 214 mCurrentPlacementBounds = bounds; 215 Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds); 216 movePipTo(adjustedBounds, animationDuration); 217 } 218 219 /** Animates the PiP to the given bounds with the given animation duration. */ movePipTo(Rect bounds, int animationDuration)220 private void movePipTo(Rect bounds, int animationDuration) { 221 if (Objects.equals(mPipTargetBounds, bounds)) { 222 return; 223 } 224 225 mPipTargetBounds = bounds; 226 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 227 "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); 228 229 if (mListener != null) { 230 mListener.onPipTargetBoundsChange(bounds, animationDuration); 231 } 232 } 233 234 /** 235 * Interface being notified of changes to the PiP bounds as calculated by 236 * @link TvPipBoundsController}. 237 */ 238 public interface PipBoundsListener { 239 /** 240 * Called when the calculated PiP bounds are changing. 241 * 242 * @param newTargetBounds The new bounds of the PiP. 243 * @param animationDuration The animation duration for the PiP movement. 244 */ onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration)245 void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration); 246 } 247 } 248