1 /* 2 * Copyright (C) 2023 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.pip2.phone; 18 19 import android.app.PictureInPictureParams; 20 import android.content.Context; 21 import android.graphics.Matrix; 22 import android.graphics.Rect; 23 import android.os.Bundle; 24 import android.os.SystemProperties; 25 import android.view.SurfaceControl; 26 import android.window.WindowContainerToken; 27 import android.window.WindowContainerTransaction; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.protolog.ProtoLog; 34 import com.android.wm.shell.common.ScreenshotUtils; 35 import com.android.wm.shell.common.ShellExecutor; 36 import com.android.wm.shell.common.pip.PipBoundsState; 37 import com.android.wm.shell.common.pip.PipDesktopState; 38 import com.android.wm.shell.pip.PipTransitionController; 39 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; 40 import com.android.wm.shell.pip2.animation.PipAlphaAnimator; 41 import com.android.wm.shell.protolog.ShellProtoLogGroup; 42 import com.android.wm.shell.shared.split.SplitScreenConstants; 43 import com.android.wm.shell.splitscreen.SplitScreenController; 44 45 import java.util.Optional; 46 import java.util.function.Supplier; 47 48 /** 49 * Scheduler for Shell initiated PiP transitions and animations. 50 */ 51 public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener { 52 private static final String TAG = PipScheduler.class.getSimpleName(); 53 54 /** 55 * The fixed start delay in ms when fading out the content overlay from bounds animation. 56 * The fadeout animation is guaranteed to start after the client has drawn under the new config. 57 */ 58 public static final int EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 59 SystemProperties.getInt( 60 "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400); 61 private static final int CONTENT_OVERLAY_FADE_OUT_DURATION_MS = 500; 62 63 private final Context mContext; 64 private final PipBoundsState mPipBoundsState; 65 private final ShellExecutor mMainExecutor; 66 private final PipTransitionState mPipTransitionState; 67 private final PipDesktopState mPipDesktopState; 68 private final Optional<SplitScreenController> mSplitScreenControllerOptional; 69 private PipTransitionController mPipTransitionController; 70 private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory 71 mSurfaceControlTransactionFactory; 72 private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; 73 74 @Nullable private Runnable mUpdateMovementBoundsRunnable; 75 @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator; 76 77 private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier; 78 private Supplier<PictureInPictureParams> mPipParamsSupplier; 79 PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState)80 public PipScheduler(Context context, 81 PipBoundsState pipBoundsState, 82 ShellExecutor mainExecutor, 83 PipTransitionState pipTransitionState, 84 Optional<SplitScreenController> splitScreenControllerOptional, 85 PipDesktopState pipDesktopState) { 86 mContext = context; 87 mPipBoundsState = pipBoundsState; 88 mMainExecutor = mainExecutor; 89 mPipTransitionState = pipTransitionState; 90 mPipTransitionState.addPipTransitionStateChangedListener(this); 91 mPipDesktopState = pipDesktopState; 92 mSplitScreenControllerOptional = splitScreenControllerOptional; 93 94 mSurfaceControlTransactionFactory = 95 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); 96 mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); 97 mPipAlphaAnimatorSupplier = PipAlphaAnimator::new; 98 } 99 setPipTransitionController(PipTransitionController pipTransitionController)100 void setPipTransitionController(PipTransitionController pipTransitionController) { 101 mPipTransitionController = pipTransitionController; 102 } 103 104 @Nullable getExitPipViaExpandTransaction()105 private WindowContainerTransaction getExitPipViaExpandTransaction() { 106 WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); 107 if (pipTaskToken == null) { 108 return null; 109 } 110 WindowContainerTransaction wct = new WindowContainerTransaction(); 111 // final expanded bounds to be inherited from the parent 112 wct.setBounds(pipTaskToken, null); 113 // if we are hitting a multi-activity case 114 // windowing mode change will reparent to original host task 115 wct.setWindowingMode(pipTaskToken, mPipDesktopState.getOutPipWindowingMode()); 116 return wct; 117 } 118 119 /** 120 * Schedules exit PiP via expand transition. 121 */ scheduleExitPipViaExpand()122 public void scheduleExitPipViaExpand() { 123 mMainExecutor.execute(() -> { 124 if (!mPipTransitionState.isInPip()) return; 125 126 final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction(); 127 if (expandWct == null) return; 128 129 final WindowContainerTransaction wct = new WindowContainerTransaction(); 130 mSplitScreenControllerOptional.ifPresent(splitScreenController -> { 131 int lastParentTaskId = mPipTransitionState.getPipTaskInfo() 132 .lastParentTaskIdBeforePip; 133 if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) { 134 splitScreenController.prepareEnterSplitScreen(wct, 135 null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED); 136 } 137 }); 138 139 boolean toSplit = !wct.isEmpty(); 140 wct.merge(expandWct, true /* transfer */); 141 mPipTransitionController.startExpandTransition(wct, toSplit); 142 }); 143 } 144 145 /** Schedules remove PiP transition. */ scheduleRemovePip(boolean withFadeout)146 public void scheduleRemovePip(boolean withFadeout) { 147 mMainExecutor.execute(() -> { 148 if (!mPipTransitionState.isInPip()) return; 149 mPipTransitionController.startRemoveTransition(withFadeout); 150 }); 151 } 152 153 /** 154 * Animates resizing of the pinned stack given the duration. 155 */ scheduleAnimateResizePip(Rect toBounds)156 public void scheduleAnimateResizePip(Rect toBounds) { 157 scheduleAnimateResizePip(toBounds, false /* configAtEnd */); 158 } 159 160 /** 161 * Animates resizing of the pinned stack given the duration. 162 * 163 * @param configAtEnd true if we are delaying config updates until the transition ends. 164 */ scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd)165 public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) { 166 scheduleAnimateResizePip(toBounds, configAtEnd, 167 PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); 168 } 169 170 /** 171 * Animates resizing of the pinned stack given the duration. 172 * 173 * @param configAtEnd true if we are delaying config updates until the transition ends. 174 * @param duration the suggested duration to run the animation; the component responsible 175 * for running the animator will get this as an extra. 176 */ scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration)177 public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration) { 178 WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); 179 if (pipTaskToken == null || !mPipTransitionState.isInPip()) { 180 return; 181 } 182 WindowContainerTransaction wct = new WindowContainerTransaction(); 183 if (configAtEnd) { 184 wct.deferConfigToTransitionEnd(pipTaskToken); 185 186 if (mPipBoundsState.getBounds().width() == toBounds.width() 187 && mPipBoundsState.getBounds().height() == toBounds.height()) { 188 // TODO (b/393159816): Config-at-End causes a flicker without size change. 189 // If PiP size isn't changing enforce a minimal one-pixel change as a workaround. 190 --toBounds.bottom; 191 } 192 } 193 wct.setBounds(pipTaskToken, toBounds); 194 mPipTransitionController.startResizeTransition(wct, duration); 195 } 196 197 /** 198 * Signals to Core to finish the PiP resize transition. 199 * Note that we do not allow any actual WM Core changes at this point. 200 * 201 * @param toBounds destination bounds used only for internal state updates - not sent to Core. 202 */ scheduleFinishResizePip(Rect toBounds)203 public void scheduleFinishResizePip(Rect toBounds) { 204 // Make updates to the internal state to reflect new bounds before updating any transitions 205 // related state; transition state updates can trigger callbacks that use the cached bounds. 206 onFinishingPipResize(toBounds); 207 mPipTransitionController.finishTransition(); 208 } 209 210 /** 211 * Directly perform a scaled matrix transformation on the leash. This will not perform any 212 * {@link WindowContainerTransaction}. 213 */ scheduleUserResizePip(Rect toBounds)214 public void scheduleUserResizePip(Rect toBounds) { 215 scheduleUserResizePip(toBounds, 0f /* degrees */); 216 } 217 218 /** 219 * Directly perform a scaled matrix transformation on the leash. This will not perform any 220 * {@link WindowContainerTransaction}. 221 * 222 * @param degrees the angle to rotate the bounds to. 223 */ scheduleUserResizePip(Rect toBounds, float degrees)224 public void scheduleUserResizePip(Rect toBounds, float degrees) { 225 if (toBounds.isEmpty() || !mPipTransitionState.isInPip()) { 226 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 227 "%s: Attempted to user resize PIP in invalid state, aborting;" 228 + "toBounds=%s, mPipTransitionState=%s", 229 TAG, toBounds, mPipTransitionState); 230 return; 231 } 232 SurfaceControl leash = mPipTransitionState.getPinnedTaskLeash(); 233 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 234 235 Matrix transformTensor = new Matrix(); 236 final float[] mMatrixTmp = new float[9]; 237 final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width(); 238 239 transformTensor.setScale(scale, scale); 240 transformTensor.postTranslate(toBounds.left, toBounds.top); 241 transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY()); 242 243 mPipSurfaceTransactionHelper.round(tx, leash, mPipBoundsState.getBounds(), toBounds); 244 245 tx.setMatrix(leash, transformTensor, mMatrixTmp); 246 tx.apply(); 247 } 248 startOverlayFadeoutAnimation(@onNull SurfaceControl overlayLeash, boolean withStartDelay, @NonNull Runnable onAnimationEnd)249 void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash, 250 boolean withStartDelay, @NonNull Runnable onAnimationEnd) { 251 mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash, 252 null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT); 253 mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS); 254 mOverlayFadeoutAnimator.setStartDelay(withStartDelay 255 ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); 256 mOverlayFadeoutAnimator.setAnimationEndCallback(() -> { 257 onAnimationEnd.run(); 258 mOverlayFadeoutAnimator = null; 259 }); 260 mOverlayFadeoutAnimator.start(); 261 } 262 setUpdateMovementBoundsRunnable(@ullable Runnable updateMovementBoundsRunnable)263 void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) { 264 mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; 265 } 266 maybeUpdateMovementBounds()267 private void maybeUpdateMovementBounds() { 268 if (mUpdateMovementBoundsRunnable != null) { 269 mUpdateMovementBoundsRunnable.run(); 270 } 271 } 272 onFinishingPipResize(Rect newBounds)273 private void onFinishingPipResize(Rect newBounds) { 274 if (mPipBoundsState.getBounds().equals(newBounds)) { 275 return; 276 } 277 278 // Take a screenshot of PiP and fade it out after resize is finished if seamless resize 279 // is off and if the PiP size is changing. 280 boolean animateCrossFadeResize = !getPipParams().isSeamlessResizeEnabled() 281 && !(mPipBoundsState.getBounds().width() == newBounds.width() 282 && mPipBoundsState.getBounds().height() == newBounds.height()); 283 if (animateCrossFadeResize) { 284 final Rect crop = new Rect(newBounds); 285 crop.offsetTo(0, 0); 286 // Note: Put this at layer=MAX_VALUE-2 since the input consumer for PIP is placed at 287 // MAX_VALUE-1 288 final SurfaceControl snapshotSurface = ScreenshotUtils.takeScreenshot( 289 mSurfaceControlTransactionFactory.getTransaction(), 290 mPipTransitionState.getPinnedTaskLeash(), crop, Integer.MAX_VALUE - 2); 291 startOverlayFadeoutAnimation(snapshotSurface, false /* withStartDelay */, () -> { 292 mSurfaceControlTransactionFactory.getTransaction().remove(snapshotSurface).apply(); 293 }); 294 } 295 296 mPipBoundsState.setBounds(newBounds); 297 maybeUpdateMovementBounds(); 298 } 299 300 @VisibleForTesting setSurfaceControlTransactionFactory( @onNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory)301 void setSurfaceControlTransactionFactory( 302 @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { 303 mSurfaceControlTransactionFactory = factory; 304 } 305 306 @Override onPipTransitionStateChanged(@ipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @android.annotation.Nullable Bundle extra)307 public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, 308 @PipTransitionState.TransitionState int newState, 309 @android.annotation.Nullable Bundle extra) { 310 switch (newState) { 311 case PipTransitionState.EXITING_PIP: 312 case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: 313 if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) { 314 mOverlayFadeoutAnimator.end(); 315 mOverlayFadeoutAnimator = null; 316 } 317 break; 318 } 319 } 320 321 @VisibleForTesting 322 interface PipAlphaAnimatorSupplier { get(@onNull Context context, SurfaceControl leash, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, @PipAlphaAnimator.Fade int direction)323 PipAlphaAnimator get(@NonNull Context context, 324 SurfaceControl leash, 325 SurfaceControl.Transaction startTransaction, 326 SurfaceControl.Transaction finishTransaction, 327 @PipAlphaAnimator.Fade int direction); 328 } 329 330 @VisibleForTesting setPipAlphaAnimatorSupplier(@onNull PipAlphaAnimatorSupplier supplier)331 void setPipAlphaAnimatorSupplier(@NonNull PipAlphaAnimatorSupplier supplier) { 332 mPipAlphaAnimatorSupplier = supplier; 333 } 334 335 @VisibleForTesting setOverlayFadeoutAnimator(@onNull PipAlphaAnimator animator)336 void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) { 337 mOverlayFadeoutAnimator = animator; 338 } 339 340 @VisibleForTesting 341 @Nullable getOverlayFadeoutAnimator()342 PipAlphaAnimator getOverlayFadeoutAnimator() { 343 return mOverlayFadeoutAnimator; 344 } 345 setPipParamsSupplier(@onNull Supplier<PictureInPictureParams> pipParamsSupplier)346 void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) { 347 mPipParamsSupplier = pipParamsSupplier; 348 } 349 350 @NonNull getPipParams()351 private PictureInPictureParams getPipParams() { 352 if (mPipParamsSupplier == null) return new PictureInPictureParams.Builder().build(); 353 return mPipParamsSupplier.get(); 354 } 355 } 356