1 /* 2 * Copyright (C) 2020 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.quickstep.util; 18 19 import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP; 20 21 import android.animation.Animator; 22 import android.animation.RectEvaluator; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.pm.ActivityInfo; 26 import android.graphics.Matrix; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.os.SystemProperties; 30 import android.util.Log; 31 import android.view.Surface; 32 import android.view.SurfaceControl; 33 import android.view.View; 34 import android.window.PictureInPictureSurfaceTransaction; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 39 import com.android.launcher3.anim.AnimationSuccessListener; 40 import com.android.launcher3.icons.IconProvider; 41 import com.android.quickstep.TaskAnimationManager; 42 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper; 43 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 44 import com.android.wm.shell.pip.PipContentOverlay; 45 46 /** 47 * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window 48 * when swiping up (in gesture navigation mode). 49 */ 50 public class SwipePipToHomeAnimator extends RectFSpringAnim { 51 private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName(); 52 53 private static final float END_PROGRESS = 1.0f; 54 55 private final int mTaskId; 56 private final ActivityInfo mActivityInfo; 57 private final SurfaceControl mLeash; 58 private final Rect mSourceRectHint = new Rect(); 59 private final Rect mAppBounds = new Rect(); 60 private final Matrix mHomeToWindowPositionMap = new Matrix(); 61 private final Rect mStartBounds = new Rect(); 62 private final RectF mCurrentBoundsF = new RectF(); 63 private final Rect mCurrentBounds = new Rect(); 64 private final Rect mDestinationBounds = new Rect(); 65 private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; 66 67 /** 68 * For calculating transform in 69 * {@link #onAnimationUpdate(SurfaceControl.Transaction, RectF, float)} 70 */ 71 private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); 72 private final Rect mSourceHintRectInsets; 73 private final Rect mSourceInsets = new Rect(); 74 75 /** for rotation calculations */ 76 private final @RecentsOrientedState.SurfaceRotation int mFromRotation; 77 private final Rect mDestinationBoundsTransformed = new Rect(); 78 79 /** 80 * Flag to avoid the double-end problem since the leash would have been released 81 * after the first end call and any further operations upon it would lead to NPE. 82 */ 83 private boolean mHasAnimationEnded; 84 85 /** 86 * Wrapper of {@link SurfaceControl} that is used when entering PiP without valid 87 * source rect hint. 88 */ 89 @Nullable 90 private PipContentOverlay mPipContentOverlay; 91 92 /** 93 * @param context {@link Context} provides Launcher resources 94 * @param taskId Task id associated with this animator, see also {@link #getTaskId()} 95 * @param activityInfo {@link ActivityInfo} associated with this animator, 96 * see also {@link #getComponentName()} 97 * @param appIconSizePx The size in pixel for the app icon in content overlay 98 * @param leash {@link SurfaceControl} this animator operates on 99 * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams} 100 * @param appBounds Bounds of the application, sourceRectHint is based on this bounds 101 * @param homeToWindowPositionMap {@link Matrix} to map a Rect from home to window space 102 * @param startBounds Bounds of the application when this animator starts. This can be 103 * different from the appBounds if user has swiped a certain distance and 104 * Launcher has performed transform on the leash. 105 * @param destinationBounds Bounds of the destination this animator ends to 106 * @param fromRotation From rotation if different from final rotation, ROTATION_0 otherwise 107 * @param destinationBoundsTransformed Destination bounds in window space 108 * @param cornerRadius Corner radius in pixel value for PiP window 109 * @param shadowRadius Shadow radius in pixel value for PiP window 110 * @param view Attached view for logging purpose 111 */ SwipePipToHomeAnimator(@onNull Context context, int taskId, @NonNull ActivityInfo activityInfo, int appIconSizePx, @NonNull SurfaceControl leash, @Nullable Rect sourceRectHint, @NonNull Rect appBounds, @NonNull Matrix homeToWindowPositionMap, @NonNull RectF startBounds, @NonNull Rect destinationBounds, @RecentsOrientedState.SurfaceRotation int fromRotation, @NonNull Rect destinationBoundsTransformed, int cornerRadius, int shadowRadius, @NonNull View view)112 private SwipePipToHomeAnimator(@NonNull Context context, 113 int taskId, 114 @NonNull ActivityInfo activityInfo, 115 int appIconSizePx, 116 @NonNull SurfaceControl leash, 117 @Nullable Rect sourceRectHint, 118 @NonNull Rect appBounds, 119 @NonNull Matrix homeToWindowPositionMap, 120 @NonNull RectF startBounds, 121 @NonNull Rect destinationBounds, 122 @RecentsOrientedState.SurfaceRotation int fromRotation, 123 @NonNull Rect destinationBoundsTransformed, 124 int cornerRadius, 125 int shadowRadius, 126 @NonNull View view) { 127 super(new DefaultSpringConfig(context, null, startBounds, 128 new RectF(destinationBoundsTransformed))); 129 mTaskId = taskId; 130 mActivityInfo = activityInfo; 131 mLeash = leash; 132 mAppBounds.set(appBounds); 133 mHomeToWindowPositionMap.set(homeToWindowPositionMap); 134 startBounds.round(mStartBounds); 135 mDestinationBounds.set(destinationBounds); 136 mFromRotation = fromRotation; 137 mDestinationBoundsTransformed.set(destinationBoundsTransformed); 138 mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius); 139 140 if (sourceRectHint != null && (sourceRectHint.width() < destinationBounds.width() 141 || sourceRectHint.height() < destinationBounds.height())) { 142 // This is a situation in which the source hint rect on at least one axis is smaller 143 // than the destination bounds, which presents a problem because we would have to scale 144 // up that axis to fit the bounds. So instead, just fallback to the non-source hint 145 // animation in this case. 146 sourceRectHint = null; 147 } 148 149 if (sourceRectHint == null) { 150 mSourceRectHint.setEmpty(); 151 mSourceHintRectInsets = null; 152 153 // Create a new overlay layer. We do not call detach on this instance, it's propagated 154 // to other classes like PipTaskOrganizer / RecentsAnimationController to complete 155 // the cleanup. 156 if (SystemProperties.getBoolean( 157 "persist.wm.debug.enable_pip_app_icon_overlay", true)) { 158 mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(), 159 mAppBounds, new IconProvider(context).getIcon(mActivityInfo), 160 appIconSizePx); 161 } else { 162 mPipContentOverlay = new PipContentOverlay.PipColorOverlay(view.getContext()); 163 } 164 final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); 165 mPipContentOverlay.attach(tx, mLeash); 166 } else { 167 mSourceRectHint.set(sourceRectHint); 168 mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left, 169 sourceRectHint.top - appBounds.top, 170 appBounds.right - sourceRectHint.right, 171 appBounds.bottom - sourceRectHint.bottom); 172 } 173 174 addAnimatorListener(new AnimationSuccessListener() { 175 @Override 176 public void onAnimationStart(Animator animation) { 177 InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP); 178 super.onAnimationStart(animation); 179 } 180 181 @Override 182 public void onAnimationCancel(Animator animation) { 183 super.onAnimationCancel(animation); 184 InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP); 185 } 186 187 @Override 188 public void onAnimationSuccess(Animator animator) { 189 InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP); 190 } 191 192 @Override 193 public void onAnimationEnd(Animator animation) { 194 if (mHasAnimationEnded) return; 195 super.onAnimationEnd(animation); 196 mHasAnimationEnded = true; 197 } 198 }); 199 addOnUpdateListener(this::onAnimationUpdate); 200 } 201 onAnimationUpdate(RectF currentRect, float progress)202 private void onAnimationUpdate(RectF currentRect, float progress) { 203 if (mHasAnimationEnded) return; 204 final SurfaceControl.Transaction tx = 205 PipSurfaceTransactionHelper.newSurfaceControlTransaction(); 206 mHomeToWindowPositionMap.mapRect(mCurrentBoundsF, currentRect); 207 onAnimationUpdate(tx, mCurrentBoundsF, progress); 208 tx.apply(); 209 } 210 onAnimationUpdate(SurfaceControl.Transaction tx, RectF currentRect, float progress)211 private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx, 212 RectF currentRect, float progress) { 213 currentRect.round(mCurrentBounds); 214 if (mPipContentOverlay != null) { 215 mPipContentOverlay.onAnimationUpdate(tx, mCurrentBounds, progress); 216 } 217 final PictureInPictureSurfaceTransaction op; 218 if (mSourceHintRectInsets == null) { 219 // no source rect hint been set, directly scale the window down 220 op = onAnimationScale(progress, tx, mCurrentBounds); 221 } else { 222 // scale and crop according to the source rect hint 223 op = onAnimationScaleAndCrop(progress, tx, mCurrentBounds); 224 } 225 return op; 226 } 227 228 /** scale the window directly with no source rect hint being set */ onAnimationScale( float progress, SurfaceControl.Transaction tx, Rect bounds)229 private PictureInPictureSurfaceTransaction onAnimationScale( 230 float progress, SurfaceControl.Transaction tx, Rect bounds) { 231 if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) { 232 final RotatedPosition rotatedPosition = getRotatedPosition(progress); 233 return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds, 234 rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY); 235 } else { 236 return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds); 237 } 238 } 239 240 /** scale and crop the window with source rect hint */ onAnimationScaleAndCrop( float progress, SurfaceControl.Transaction tx, Rect bounds)241 private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop( 242 float progress, SurfaceControl.Transaction tx, 243 Rect bounds) { 244 final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets, 245 mSourceHintRectInsets); 246 if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) { 247 final RotatedPosition rotatedPosition = getRotatedPosition(progress); 248 return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets, 249 rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY); 250 } else { 251 return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, mAppBounds, 252 bounds, insets, progress); 253 } 254 } 255 getTaskId()256 public int getTaskId() { 257 return mTaskId; 258 } 259 getComponentName()260 public ComponentName getComponentName() { 261 return mActivityInfo.getComponentName(); 262 } 263 getDestinationBounds()264 public Rect getDestinationBounds() { 265 return mDestinationBounds; 266 } 267 268 @Nullable getContentOverlay()269 public SurfaceControl getContentOverlay() { 270 return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash(); 271 } 272 273 /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */ getFinishTransaction()274 public PictureInPictureSurfaceTransaction getFinishTransaction() { 275 // get the final leash operations but do not apply to the leash. 276 final SurfaceControl.Transaction tx = 277 PipSurfaceTransactionHelper.newSurfaceControlTransaction(); 278 final PictureInPictureSurfaceTransaction pipTx = 279 onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS); 280 pipTx.setShouldDisableCanAffectSystemUiFlags(true); 281 return pipTx; 282 } 283 getRotatedPosition(float progress)284 private RotatedPosition getRotatedPosition(float progress) { 285 final float degree, positionX, positionY; 286 if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) { 287 if (mFromRotation == Surface.ROTATION_90) { 288 degree = -90 * (1 - progress); 289 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 290 + mStartBounds.left; 291 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 292 + mStartBounds.top + mStartBounds.bottom * (1 - progress); 293 } else { 294 degree = 90 * (1 - progress); 295 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 296 + mStartBounds.left + mStartBounds.right * (1 - progress); 297 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 298 + mStartBounds.top; 299 } 300 } else { 301 if (mFromRotation == Surface.ROTATION_90) { 302 degree = -90 * progress; 303 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left) 304 + mStartBounds.left; 305 positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top) 306 + mStartBounds.top; 307 } else { 308 degree = 90 * progress; 309 positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left) 310 + mStartBounds.left; 311 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top) 312 + mStartBounds.top; 313 } 314 } 315 316 return new RotatedPosition(degree, positionX, positionY); 317 } 318 319 /** Builder class for {@link SwipePipToHomeAnimator} */ 320 public static class Builder { 321 private Context mContext; 322 private int mTaskId; 323 private ActivityInfo mActivityInfo; 324 private int mAppIconSizePx; 325 private SurfaceControl mLeash; 326 private Rect mSourceRectHint; 327 private Rect mDisplayCutoutInsets; 328 private Rect mAppBounds; 329 private Matrix mHomeToWindowPositionMap; 330 private RectF mStartBounds; 331 private Rect mDestinationBounds; 332 private int mCornerRadius; 333 private int mShadowRadius; 334 private View mAttachedView; 335 private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0; 336 private final Rect mDestinationBoundsTransformed = new Rect(); 337 setContext(Context context)338 public Builder setContext(Context context) { 339 mContext = context; 340 return this; 341 } 342 setTaskId(int taskId)343 public Builder setTaskId(int taskId) { 344 mTaskId = taskId; 345 return this; 346 } 347 setActivityInfo(ActivityInfo activityInfo)348 public Builder setActivityInfo(ActivityInfo activityInfo) { 349 mActivityInfo = activityInfo; 350 return this; 351 } 352 setAppIconSizePx(int appIconSizePx)353 public Builder setAppIconSizePx(int appIconSizePx) { 354 mAppIconSizePx = appIconSizePx; 355 return this; 356 } 357 setLeash(SurfaceControl leash)358 public Builder setLeash(SurfaceControl leash) { 359 mLeash = leash; 360 return this; 361 } 362 setSourceRectHint(Rect sourceRectHint)363 public Builder setSourceRectHint(Rect sourceRectHint) { 364 mSourceRectHint = new Rect(sourceRectHint); 365 return this; 366 } 367 setAppBounds(Rect appBounds)368 public Builder setAppBounds(Rect appBounds) { 369 mAppBounds = new Rect(appBounds); 370 return this; 371 } 372 setHomeToWindowPositionMap(Matrix homeToWindowPositionMap)373 public Builder setHomeToWindowPositionMap(Matrix homeToWindowPositionMap) { 374 mHomeToWindowPositionMap = new Matrix(homeToWindowPositionMap); 375 return this; 376 } 377 setStartBounds(RectF startBounds)378 public Builder setStartBounds(RectF startBounds) { 379 mStartBounds = new RectF(startBounds); 380 return this; 381 } 382 setDestinationBounds(Rect destinationBounds)383 public Builder setDestinationBounds(Rect destinationBounds) { 384 mDestinationBounds = new Rect(destinationBounds); 385 return this; 386 } 387 setCornerRadius(int cornerRadius)388 public Builder setCornerRadius(int cornerRadius) { 389 mCornerRadius = cornerRadius; 390 return this; 391 } 392 setShadowRadius(int shadowRadius)393 public Builder setShadowRadius(int shadowRadius) { 394 mShadowRadius = shadowRadius; 395 return this; 396 } 397 setAttachedView(View attachedView)398 public Builder setAttachedView(View attachedView) { 399 mAttachedView = attachedView; 400 return this; 401 } 402 setFromRotation(TaskViewSimulator taskViewSimulator, @RecentsOrientedState.SurfaceRotation int fromRotation, Rect displayCutoutInsets)403 public Builder setFromRotation(TaskViewSimulator taskViewSimulator, 404 @RecentsOrientedState.SurfaceRotation int fromRotation, 405 Rect displayCutoutInsets) { 406 if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) { 407 Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation); 408 return this; 409 } 410 final Matrix matrix = new Matrix(); 411 taskViewSimulator.applyWindowToHomeRotation(matrix); 412 413 // map the destination bounds into window space. mDestinationBounds is always calculated 414 // in the final home space and the animation runs in original window space. 415 final RectF transformed = new RectF(mDestinationBounds); 416 matrix.mapRect(transformed, new RectF(mDestinationBounds)); 417 transformed.round(mDestinationBoundsTransformed); 418 419 mFromRotation = fromRotation; 420 if (displayCutoutInsets != null) { 421 mDisplayCutoutInsets = new Rect(displayCutoutInsets); 422 } 423 return this; 424 } 425 build()426 public SwipePipToHomeAnimator build() { 427 if (mDestinationBoundsTransformed.isEmpty()) { 428 mDestinationBoundsTransformed.set(mDestinationBounds); 429 } 430 // adjust the mSourceRectHint / mAppBounds by display cutout if applicable. 431 if (mSourceRectHint != null && mDisplayCutoutInsets != null) { 432 if (mFromRotation == Surface.ROTATION_90) { 433 mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top); 434 } else if (mFromRotation == Surface.ROTATION_270) { 435 mAppBounds.inset(mDisplayCutoutInsets); 436 } 437 } 438 return new SwipePipToHomeAnimator(mContext, mTaskId, mActivityInfo, mAppIconSizePx, 439 mLeash, mSourceRectHint, mAppBounds, 440 mHomeToWindowPositionMap, mStartBounds, mDestinationBounds, 441 mFromRotation, mDestinationBoundsTransformed, 442 mCornerRadius, mShadowRadius, mAttachedView); 443 } 444 } 445 446 private static class RotatedPosition { 447 private final float degree; 448 private final float positionX; 449 private final float positionY; 450 RotatedPosition(float degree, float positionX, float positionY)451 private RotatedPosition(float degree, float positionX, float positionY) { 452 this.degree = degree; 453 this.positionX = positionX; 454 this.positionY = positionY; 455 } 456 } 457 } 458