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