1 /*
2  * Copyright (C) 2018 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.systemui.statusbar.notification;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.app.ActivityManager;
23 import android.graphics.Matrix;
24 import android.graphics.Rect;
25 import android.os.RemoteException;
26 import android.util.MathUtils;
27 import android.view.IRemoteAnimationFinishedCallback;
28 import android.view.IRemoteAnimationRunner;
29 import android.view.RemoteAnimationAdapter;
30 import android.view.RemoteAnimationTarget;
31 import android.view.SyncRtSurfaceTransactionApplier;
32 import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
33 import android.view.View;
34 
35 import com.android.internal.policy.ScreenDecorationsUtils;
36 import com.android.systemui.Interpolators;
37 import com.android.systemui.shared.system.SurfaceControlCompat;
38 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
39 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
40 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
41 import com.android.systemui.statusbar.phone.NotificationPanelView;
42 import com.android.systemui.statusbar.phone.StatusBarWindowView;
43 
44 /**
45  * A class that allows activities to be launched in a seamless way where the notification
46  * transforms nicely into the starting window.
47  */
48 public class ActivityLaunchAnimator {
49 
50     private static final int ANIMATION_DURATION = 400;
51     public static final long ANIMATION_DURATION_FADE_CONTENT = 67;
52     public static final long ANIMATION_DURATION_FADE_APP = 200;
53     public static final long ANIMATION_DELAY_ICON_FADE_IN = ANIMATION_DURATION -
54             CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY
55             - 16;
56     private static final long LAUNCH_TIMEOUT = 500;
57     private final NotificationPanelView mNotificationPanel;
58     private final NotificationListContainer mNotificationContainer;
59     private final StatusBarWindowView mStatusBarWindow;
60     private final float mWindowCornerRadius;
61     private Callback mCallback;
62     private final Runnable mTimeoutRunnable = () -> {
63         setAnimationPending(false);
64         mCallback.onExpandAnimationTimedOut();
65     };
66     private boolean mAnimationPending;
67     private boolean mAnimationRunning;
68     private boolean mIsLaunchForActivity;
69 
ActivityLaunchAnimator(StatusBarWindowView statusBarWindow, Callback callback, NotificationPanelView notificationPanel, NotificationListContainer container)70     public ActivityLaunchAnimator(StatusBarWindowView statusBarWindow,
71             Callback callback,
72             NotificationPanelView notificationPanel,
73             NotificationListContainer container) {
74         mNotificationPanel = notificationPanel;
75         mNotificationContainer = container;
76         mStatusBarWindow = statusBarWindow;
77         mCallback = callback;
78         mWindowCornerRadius = ScreenDecorationsUtils
79                 .getWindowCornerRadius(statusBarWindow.getResources());
80     }
81 
getLaunchAnimation( View sourceView, boolean occluded)82     public RemoteAnimationAdapter getLaunchAnimation(
83             View sourceView, boolean occluded) {
84         if (!(sourceView instanceof ExpandableNotificationRow) || !mCallback.areLaunchAnimationsEnabled() || occluded) {
85             return null;
86         }
87         AnimationRunner animationRunner = new AnimationRunner(
88                 (ExpandableNotificationRow) sourceView);
89         return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
90                 ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
91     }
92 
isAnimationPending()93     public boolean isAnimationPending() {
94         return mAnimationPending;
95     }
96 
97     /**
98      * Set the launch result the intent requested
99      *
100      * @param launchResult the launch result
101      * @param wasIntentActivity was this launch for an activity
102      */
setLaunchResult(int launchResult, boolean wasIntentActivity)103     public void setLaunchResult(int launchResult, boolean wasIntentActivity) {
104         mIsLaunchForActivity = wasIntentActivity;
105         setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT
106                 || launchResult == ActivityManager.START_SUCCESS)
107                         && mCallback.areLaunchAnimationsEnabled());
108     }
109 
isLaunchForActivity()110     public boolean isLaunchForActivity() {
111         return mIsLaunchForActivity;
112     }
113 
setAnimationPending(boolean pending)114     private void setAnimationPending(boolean pending) {
115         mAnimationPending = pending;
116         mStatusBarWindow.setExpandAnimationPending(pending);
117         if (pending) {
118             mStatusBarWindow.postDelayed(mTimeoutRunnable, LAUNCH_TIMEOUT);
119         } else {
120             mStatusBarWindow.removeCallbacks(mTimeoutRunnable);
121         }
122     }
123 
isAnimationRunning()124     public boolean isAnimationRunning() {
125         return mAnimationRunning;
126     }
127 
128     class AnimationRunner extends IRemoteAnimationRunner.Stub {
129 
130         private final ExpandableNotificationRow mSourceNotification;
131         private final ExpandAnimationParameters mParams;
132         private final Rect mWindowCrop = new Rect();
133         private final float mNotificationCornerRadius;
134         private float mCornerRadius;
135         private boolean mIsFullScreenLaunch = true;
136         private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
137 
AnimationRunner(ExpandableNotificationRow sourceNofitication)138         public AnimationRunner(ExpandableNotificationRow sourceNofitication) {
139             mSourceNotification = sourceNofitication;
140             mParams = new ExpandAnimationParameters();
141             mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
142             mNotificationCornerRadius = Math.max(mSourceNotification.getCurrentTopRoundness(),
143                     mSourceNotification.getCurrentBottomRoundness());
144         }
145 
146         @Override
onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets, IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)147         public void onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets,
148                 IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
149                     throws RemoteException {
150             mSourceNotification.post(() -> {
151                 RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
152                         remoteAnimationTargets);
153                 if (primary == null) {
154                     setAnimationPending(false);
155                     invokeCallback(iRemoteAnimationFinishedCallback);
156                     mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
157                     return;
158                 }
159 
160                 setExpandAnimationRunning(true);
161                 mIsFullScreenLaunch = primary.position.y == 0
162                         && primary.sourceContainerBounds.height()
163                                 >= mNotificationPanel.getHeight();
164                 if (!mIsFullScreenLaunch) {
165                     mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
166                 }
167                 ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
168                 mParams.startPosition = mSourceNotification.getLocationOnScreen();
169                 mParams.startTranslationZ = mSourceNotification.getTranslationZ();
170                 mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
171                 if (mSourceNotification.isChildInGroup()) {
172                     int parentClip = mSourceNotification
173                             .getNotificationParent().getClipTopAmount();
174                     mParams.parentStartClipTopAmount = parentClip;
175                     // We need to calculate how much the child is clipped by the parent
176                     // because children always have 0 clipTopAmount
177                     if (parentClip != 0) {
178                         float childClip = parentClip
179                                 - mSourceNotification.getTranslationY();
180                         if (childClip > 0.0f) {
181                             mParams.startClipTopAmount = (int) Math.ceil(childClip);
182                         }
183                     }
184                 }
185                 int targetWidth = primary.sourceContainerBounds.width();
186                 int notificationHeight = mSourceNotification.getActualHeight()
187                         - mSourceNotification.getClipBottomAmount();
188                 int notificationWidth = mSourceNotification.getWidth();
189                 anim.setDuration(ANIMATION_DURATION);
190                 anim.setInterpolator(Interpolators.LINEAR);
191                 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
192                     @Override
193                     public void onAnimationUpdate(ValueAnimator animation) {
194                         mParams.linearProgress = animation.getAnimatedFraction();
195                         float progress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
196                                         mParams.linearProgress);
197                         int newWidth = (int) MathUtils.lerp(notificationWidth,
198                                 targetWidth, progress);
199                         mParams.left = (int) ((targetWidth - newWidth) / 2.0f);
200                         mParams.right = mParams.left + newWidth;
201                         mParams.top = (int) MathUtils.lerp(mParams.startPosition[1],
202                                 primary.position.y, progress);
203                         mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1]
204                                         + notificationHeight,
205                                 primary.position.y + primary.sourceContainerBounds.bottom,
206                                 progress);
207                         mCornerRadius = MathUtils.lerp(mNotificationCornerRadius,
208                                 mWindowCornerRadius, progress);
209                         applyParamsToWindow(primary);
210                         applyParamsToNotification(mParams);
211                         applyParamsToNotificationList(mParams);
212                     }
213                 });
214                 anim.addListener(new AnimatorListenerAdapter() {
215                     @Override
216                     public void onAnimationEnd(Animator animation) {
217                         setExpandAnimationRunning(false);
218                         invokeCallback(iRemoteAnimationFinishedCallback);
219                     }
220                 });
221                 anim.start();
222                 setAnimationPending(false);
223             });
224         }
225 
invokeCallback(IRemoteAnimationFinishedCallback callback)226         private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
227             try {
228                 callback.onAnimationFinished();
229             } catch (RemoteException e) {
230                 e.printStackTrace();
231             }
232         }
233 
getPrimaryRemoteAnimationTarget( RemoteAnimationTarget[] remoteAnimationTargets)234         private RemoteAnimationTarget getPrimaryRemoteAnimationTarget(
235                 RemoteAnimationTarget[] remoteAnimationTargets) {
236             RemoteAnimationTarget primary = null;
237             for (RemoteAnimationTarget app : remoteAnimationTargets) {
238                 if (app.mode == RemoteAnimationTarget.MODE_OPENING) {
239                     primary = app;
240                     break;
241                 }
242             }
243             return primary;
244         }
245 
setExpandAnimationRunning(boolean running)246         private void setExpandAnimationRunning(boolean running) {
247             mNotificationPanel.setLaunchingNotification(running);
248             mSourceNotification.setExpandAnimationRunning(running);
249             mStatusBarWindow.setExpandAnimationRunning(running);
250             mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
251             mAnimationRunning = running;
252             if (!running) {
253                 mCallback.onExpandAnimationFinished(mIsFullScreenLaunch);
254                 applyParamsToNotification(null);
255                 applyParamsToNotificationList(null);
256             }
257 
258         }
259 
applyParamsToNotificationList(ExpandAnimationParameters params)260         private void applyParamsToNotificationList(ExpandAnimationParameters params) {
261             mNotificationContainer.applyExpandAnimationParams(params);
262             mNotificationPanel.applyExpandAnimationParams(params);
263         }
264 
applyParamsToNotification(ExpandAnimationParameters params)265         private void applyParamsToNotification(ExpandAnimationParameters params) {
266             mSourceNotification.applyExpandAnimationParams(params);
267         }
268 
applyParamsToWindow(RemoteAnimationTarget app)269         private void applyParamsToWindow(RemoteAnimationTarget app) {
270             Matrix m = new Matrix();
271             m.postTranslate(0, (float) (mParams.top - app.position.y));
272             mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
273             SurfaceParams params = new SurfaceParams(app.leash, 1f /* alpha */, m, mWindowCrop,
274                     app.prefixOrderIndex, mCornerRadius, true /* visible */);
275             mSyncRtTransactionApplier.scheduleApply(params);
276         }
277 
278         @Override
onAnimationCancelled()279         public void onAnimationCancelled() throws RemoteException {
280             mSourceNotification.post(() -> {
281                 setAnimationPending(false);
282                 mCallback.onLaunchAnimationCancelled();
283             });
284         }
285     };
286 
287     public static class ExpandAnimationParameters {
288         float linearProgress;
289         int[] startPosition;
290         float startTranslationZ;
291         int left;
292         int top;
293         int right;
294         int bottom;
295         int startClipTopAmount;
296         int parentStartClipTopAmount;
297 
ExpandAnimationParameters()298         public ExpandAnimationParameters() {
299         }
300 
getTop()301         public int getTop() {
302             return top;
303         }
304 
getBottom()305         public int getBottom() {
306             return bottom;
307         }
308 
getWidth()309         public int getWidth() {
310             return right - left;
311         }
312 
getHeight()313         public int getHeight() {
314             return bottom - top;
315         }
316 
getTopChange()317         public int getTopChange() {
318             // We need this compensation to ensure that the QS moves in sync.
319             int clipTopAmountCompensation = 0;
320             if (startClipTopAmount != 0.0f) {
321                 clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount,
322                         Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress));
323             }
324             return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0);
325         }
326 
getProgress()327         public float getProgress() {
328             return linearProgress;
329         }
330 
getProgress(long delay, long duration)331         public float getProgress(long delay, long duration) {
332             return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay)
333                     / duration, 0.0f, 1.0f);
334         }
335 
getStartClipTopAmount()336         public int getStartClipTopAmount() {
337             return startClipTopAmount;
338         }
339 
getParentStartClipTopAmount()340         public int getParentStartClipTopAmount() {
341             return parentStartClipTopAmount;
342         }
343 
getStartTranslationZ()344         public float getStartTranslationZ() {
345             return startTranslationZ;
346         }
347     }
348 
349     public interface Callback {
350 
351         /**
352          * Called when the launch animation was cancelled.
353          */
onLaunchAnimationCancelled()354         void onLaunchAnimationCancelled();
355 
356         /**
357          * Called when the launch animation has timed out without starting an actual animation.
358          */
onExpandAnimationTimedOut()359         void onExpandAnimationTimedOut();
360 
361         /**
362          * Called when the expand animation has finished.
363          *
364          * @param launchIsFullScreen True if this launch was fullscreen, such that now the window
365          *                           fills the whole screen
366          */
onExpandAnimationFinished(boolean launchIsFullScreen)367         void onExpandAnimationFinished(boolean launchIsFullScreen);
368 
369         /**
370          * Are animations currently enabled.
371          */
areLaunchAnimationsEnabled()372         boolean areLaunchAnimationsEnabled();
373     }
374 }
375