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 android.animation.Animator;
20 import android.animation.RectEvaluator;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.pm.ActivityInfo;
24 import android.graphics.Matrix;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.util.Log;
28 import android.view.Surface;
29 import android.view.SurfaceControl;
30 import android.view.View;
31 import android.window.PictureInPictureSurfaceTransaction;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 
36 import com.android.internal.jank.Cuj;
37 import com.android.launcher3.anim.AnimationSuccessListener;
38 import com.android.launcher3.icons.IconProvider;
39 import com.android.quickstep.TaskAnimationManager;
40 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
41 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
42 import com.android.wm.shell.pip.PipContentOverlay;
43 
44 /**
45  * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
46  * when swiping up (in gesture navigation mode).
47  */
48 public class SwipePipToHomeAnimator extends RectFSpringAnim {
49     private static final String TAG = "SwipePipToHomeAnimator";
50 
51     private static final float END_PROGRESS = 1.0f;
52 
53     private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
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, @NonNull 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             @NonNull 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         final float aspectRatio = destinationBounds.width() / (float) destinationBounds.height();
141         String reasonForCreateOverlay = null; // For debugging purpose.
142         if (sourceRectHint.isEmpty()) {
143             reasonForCreateOverlay = "Source rect hint is empty";
144         } else if (sourceRectHint.width() < destinationBounds.width()
145                 || sourceRectHint.height() < destinationBounds.height()) {
146             // This is a situation in which the source hint rect on at least one axis is smaller
147             // than the destination bounds, which presents a problem because we would have to scale
148             // up that axis to fit the bounds. So instead, just fallback to the non-source hint
149             // animation in this case.
150             reasonForCreateOverlay = "Source rect hint is too small " + sourceRectHint;
151             sourceRectHint.setEmpty();
152         } else if (!appBounds.contains(sourceRectHint)) {
153             // This is a situation in which the source hint rect is outside the app bounds, so it is
154             // not a valid rectangle to use for cropping app surface
155             reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
156             sourceRectHint.setEmpty();
157         } else if (Math.abs(
158                 aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height()))
159                 > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
160             // The source rect hint does not aspect ratio
161             reasonForCreateOverlay = "Source rect hint does not match aspect ratio "
162                     + sourceRectHint + " aspect ratio " + aspectRatio;
163             sourceRectHint.setEmpty();
164         }
165 
166         if (sourceRectHint.isEmpty()) {
167             // Crop a Rect matches the aspect ratio and pivots at the center point.
168             // To make the animation path simplified.
169             if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) {
170                 // use the full height.
171                 mSourceRectHint.set(0, 0,
172                         (int) (appBounds.height() * aspectRatio), appBounds.height());
173                 mSourceRectHint.offset(
174                         (appBounds.width() - mSourceRectHint.width()) / 2, 0);
175             } else {
176                 // use the full width.
177                 mSourceRectHint.set(0, 0,
178                         appBounds.width(), (int) (appBounds.width() / aspectRatio));
179                 mSourceRectHint.offset(
180                         0, (appBounds.height() - mSourceRectHint.height()) / 2);
181             }
182 
183             // Create a new overlay layer. We do not call detach on this instance, it's propagated
184             // to other classes like PipTaskOrganizer / RecentsAnimationController to complete
185             // the cleanup.
186             mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
187                     mAppBounds, mDestinationBounds,
188                     new IconProvider(context).getIcon(mActivityInfo), appIconSizePx);
189             final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
190             mPipContentOverlay.attach(tx, mLeash);
191             Log.d(TAG, getContentOverlay() + " is created: " + reasonForCreateOverlay);
192         } else {
193             mSourceRectHint.set(sourceRectHint);
194         }
195         mSourceHintRectInsets = new Rect(mSourceRectHint.left - appBounds.left,
196                 mSourceRectHint.top - appBounds.top,
197                 appBounds.right - mSourceRectHint.right,
198                 appBounds.bottom - mSourceRectHint.bottom);
199 
200         addAnimatorListener(new AnimationSuccessListener() {
201             @Override
202             public void onAnimationStart(Animator animation) {
203                 InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
204                 super.onAnimationStart(animation);
205             }
206 
207             @Override
208             public void onAnimationCancel(Animator animation) {
209                 super.onAnimationCancel(animation);
210                 InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
211             }
212 
213             @Override
214             public void onAnimationSuccess(Animator animator) {
215                 InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
216             }
217 
218             @Override
219             public void onAnimationEnd(Animator animation) {
220                 if (mHasAnimationEnded) return;
221                 super.onAnimationEnd(animation);
222                 mHasAnimationEnded = true;
223             }
224         });
225         addOnUpdateListener(this::onAnimationUpdate);
226     }
227 
onAnimationUpdate(RectF currentRect, float progress)228     private void onAnimationUpdate(RectF currentRect, float progress) {
229         if (mHasAnimationEnded) return;
230         final SurfaceControl.Transaction tx =
231                 PipSurfaceTransactionHelper.newSurfaceControlTransaction();
232         mHomeToWindowPositionMap.mapRect(mCurrentBoundsF, currentRect);
233         onAnimationUpdate(tx, mCurrentBoundsF, progress);
234         tx.apply();
235     }
236 
onAnimationUpdate(SurfaceControl.Transaction tx, RectF currentRect, float progress)237     private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx,
238             RectF currentRect, float progress) {
239         currentRect.round(mCurrentBounds);
240         if (mPipContentOverlay != null) {
241             mPipContentOverlay.onAnimationUpdate(tx, mCurrentBounds, progress);
242         }
243         return onAnimationScaleAndCrop(progress, tx, mCurrentBounds);
244     }
245 
246     /** scale and crop the window with source rect hint */
onAnimationScaleAndCrop( float progress, SurfaceControl.Transaction tx, Rect bounds)247     private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop(
248             float progress, SurfaceControl.Transaction tx,
249             Rect bounds) {
250         final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets,
251                 mSourceHintRectInsets);
252         if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
253             final RotatedPosition rotatedPosition = getRotatedPosition(progress);
254             return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
255                     rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
256         } else {
257             return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, mAppBounds,
258                     bounds, insets, progress);
259         }
260     }
261 
getTaskId()262     public int getTaskId() {
263         return mTaskId;
264     }
265 
getComponentName()266     public ComponentName getComponentName() {
267         return mActivityInfo.getComponentName();
268     }
269 
getDestinationBounds()270     public Rect getDestinationBounds() {
271         return mDestinationBounds;
272     }
273 
getAppBounds()274     public Rect getAppBounds() {
275         return mAppBounds;
276     }
277 
getSourceRectHint()278     public Rect getSourceRectHint() {
279         return mSourceRectHint;
280     }
281 
282     @Nullable
getContentOverlay()283     public SurfaceControl getContentOverlay() {
284         return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash();
285     }
286 
287     /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */
getFinishTransaction()288     public PictureInPictureSurfaceTransaction getFinishTransaction() {
289         // get the final leash operations but do not apply to the leash.
290         final SurfaceControl.Transaction tx =
291                 PipSurfaceTransactionHelper.newSurfaceControlTransaction();
292         final PictureInPictureSurfaceTransaction pipTx =
293                 onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS);
294         pipTx.setShouldDisableCanAffectSystemUiFlags(true);
295         return pipTx;
296     }
297 
getRotatedPosition(float progress)298     private RotatedPosition getRotatedPosition(float progress) {
299         final float degree, positionX, positionY;
300         if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) {
301             if (mFromRotation == Surface.ROTATION_90) {
302                 degree = -90 * (1 - progress);
303                 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
304                         + mStartBounds.left;
305                 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
306                         + mStartBounds.top + mStartBounds.bottom * (1 - progress);
307             } else {
308                 degree = 90 * (1 - progress);
309                 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
310                         + mStartBounds.left + mStartBounds.right * (1 - progress);
311                 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
312                         + mStartBounds.top;
313             }
314         } else {
315             if (mFromRotation == Surface.ROTATION_90) {
316                 degree = -90 * progress;
317                 positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
318                         + mStartBounds.left;
319                 positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top)
320                         + mStartBounds.top;
321             } else {
322                 degree = 90 * progress;
323                 positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left)
324                         + mStartBounds.left;
325                 positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
326                         + mStartBounds.top;
327             }
328         }
329 
330         return new RotatedPosition(degree, positionX, positionY);
331     }
332 
333     /** Builder class for {@link SwipePipToHomeAnimator} */
334     public static class Builder {
335         private Context mContext;
336         private int mTaskId;
337         private ActivityInfo mActivityInfo;
338         private int mAppIconSizePx;
339         private SurfaceControl mLeash;
340         private Rect mSourceRectHint;
341         private Rect mDisplayCutoutInsets;
342         private Rect mAppBounds;
343         private Matrix mHomeToWindowPositionMap;
344         private RectF mStartBounds;
345         private Rect mDestinationBounds;
346         private int mCornerRadius;
347         private int mShadowRadius;
348         private View mAttachedView;
349         private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0;
350         private final Rect mDestinationBoundsTransformed = new Rect();
351 
setContext(Context context)352         public Builder setContext(Context context) {
353             mContext = context;
354             return this;
355         }
356 
setTaskId(int taskId)357         public Builder setTaskId(int taskId) {
358             mTaskId = taskId;
359             return this;
360         }
361 
setActivityInfo(ActivityInfo activityInfo)362         public Builder setActivityInfo(ActivityInfo activityInfo) {
363             mActivityInfo = activityInfo;
364             return this;
365         }
366 
setAppIconSizePx(int appIconSizePx)367         public Builder setAppIconSizePx(int appIconSizePx) {
368             mAppIconSizePx = appIconSizePx;
369             return this;
370         }
371 
setLeash(SurfaceControl leash)372         public Builder setLeash(SurfaceControl leash) {
373             mLeash = leash;
374             return this;
375         }
376 
setSourceRectHint(Rect sourceRectHint)377         public Builder setSourceRectHint(Rect sourceRectHint) {
378             mSourceRectHint = new Rect(sourceRectHint);
379             return this;
380         }
381 
setAppBounds(Rect appBounds)382         public Builder setAppBounds(Rect appBounds) {
383             mAppBounds = new Rect(appBounds);
384             return this;
385         }
386 
setHomeToWindowPositionMap(Matrix homeToWindowPositionMap)387         public Builder setHomeToWindowPositionMap(Matrix homeToWindowPositionMap) {
388             mHomeToWindowPositionMap = new Matrix(homeToWindowPositionMap);
389             return this;
390         }
391 
setStartBounds(RectF startBounds)392         public Builder setStartBounds(RectF startBounds) {
393             mStartBounds = new RectF(startBounds);
394             return this;
395         }
396 
setDestinationBounds(Rect destinationBounds)397         public Builder setDestinationBounds(Rect destinationBounds) {
398             mDestinationBounds = new Rect(destinationBounds);
399             return this;
400         }
401 
setCornerRadius(int cornerRadius)402         public Builder setCornerRadius(int cornerRadius) {
403             mCornerRadius = cornerRadius;
404             return this;
405         }
406 
setShadowRadius(int shadowRadius)407         public Builder setShadowRadius(int shadowRadius) {
408             mShadowRadius = shadowRadius;
409             return this;
410         }
411 
setAttachedView(View attachedView)412         public Builder setAttachedView(View attachedView) {
413             mAttachedView = attachedView;
414             return this;
415         }
416 
setFromRotation(TaskViewSimulator taskViewSimulator, @RecentsOrientedState.SurfaceRotation int fromRotation, Rect displayCutoutInsets)417         public Builder setFromRotation(TaskViewSimulator taskViewSimulator,
418                 @RecentsOrientedState.SurfaceRotation int fromRotation,
419                 Rect displayCutoutInsets) {
420             if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) {
421                 Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation);
422                 return this;
423             }
424             final Matrix matrix = new Matrix();
425             taskViewSimulator.applyWindowToHomeRotation(matrix);
426 
427             // map the destination bounds into window space. mDestinationBounds is always calculated
428             // in the final home space and the animation runs in original window space.
429             final RectF transformed = new RectF(mDestinationBounds);
430             matrix.mapRect(transformed, new RectF(mDestinationBounds));
431             transformed.round(mDestinationBoundsTransformed);
432 
433             mFromRotation = fromRotation;
434             if (displayCutoutInsets != null) {
435                 mDisplayCutoutInsets = new Rect(displayCutoutInsets);
436             }
437             return this;
438         }
439 
build()440         public SwipePipToHomeAnimator build() {
441             if (mDestinationBoundsTransformed.isEmpty()) {
442                 mDestinationBoundsTransformed.set(mDestinationBounds);
443             }
444             // adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
445             if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
446                 if (mFromRotation == Surface.ROTATION_90) {
447                     mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
448                 } else if (mFromRotation == Surface.ROTATION_270) {
449                     mAppBounds.inset(mDisplayCutoutInsets);
450                 }
451             }
452             return new SwipePipToHomeAnimator(mContext, mTaskId, mActivityInfo, mAppIconSizePx,
453                     mLeash, mSourceRectHint, mAppBounds,
454                     mHomeToWindowPositionMap, mStartBounds, mDestinationBounds,
455                     mFromRotation, mDestinationBoundsTransformed,
456                     mCornerRadius, mShadowRadius, mAttachedView);
457         }
458     }
459 
460     private static class RotatedPosition {
461         private final float degree;
462         private final float positionX;
463         private final float positionY;
464 
RotatedPosition(float degree, float positionX, float positionY)465         private RotatedPosition(float degree, float positionX, float positionY) {
466             this.degree = degree;
467             this.positionX = positionX;
468             this.positionY = positionY;
469         }
470     }
471 }
472