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