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 
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.shared.system.SurfaceControlCompat;
34 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier;
35 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams;
36 import com.android.systemui.statusbar.ExpandableNotificationRow;
37 import com.android.systemui.statusbar.NotificationListContainer;
38 import com.android.systemui.statusbar.StatusBarState;
39 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
40 import com.android.systemui.statusbar.phone.NotificationPanelView;
41 import com.android.systemui.statusbar.phone.StatusBar;
42 import com.android.systemui.statusbar.phone.StatusBarWindowView;
43 
44 import java.util.ArrayList;
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 NotificationPanelView mNotificationPanel;
60     private final NotificationListContainer mNotificationContainer;
61     private final StatusBarWindowView mStatusBarWindow;
62     private StatusBar mStatusBar;
63     private final Runnable mTimeoutRunnable = () -> {
64         setAnimationPending(false);
65         mStatusBar.collapsePanel(true /* animate */);
66     };
67     private boolean mAnimationPending;
68 
ActivityLaunchAnimator(StatusBarWindowView statusBarWindow, StatusBar statusBar, NotificationPanelView notificationPanel, NotificationListContainer container)69     public ActivityLaunchAnimator(StatusBarWindowView statusBarWindow,
70             StatusBar statusBar,
71             NotificationPanelView notificationPanel,
72             NotificationListContainer container) {
73         mNotificationPanel = notificationPanel;
74         mNotificationContainer = container;
75         mStatusBarWindow = statusBarWindow;
76         mStatusBar = statusBar;
77     }
78 
getLaunchAnimation( ExpandableNotificationRow sourceNotification, boolean occluded)79     public RemoteAnimationAdapter getLaunchAnimation(
80             ExpandableNotificationRow sourceNotification, boolean occluded) {
81         if (mStatusBar.getBarState() != StatusBarState.SHADE || occluded) {
82             return null;
83         }
84         AnimationRunner animationRunner = new AnimationRunner(sourceNotification);
85         return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
86                 ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
87     }
88 
isAnimationPending()89     public boolean isAnimationPending() {
90         return mAnimationPending;
91     }
92 
setLaunchResult(int launchResult)93     public void setLaunchResult(int launchResult) {
94         setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT
95                 || launchResult == ActivityManager.START_SUCCESS)
96                         && mStatusBar.getBarState() == StatusBarState.SHADE);
97     }
98 
setAnimationPending(boolean pending)99     private void setAnimationPending(boolean pending) {
100         mAnimationPending = pending;
101         mStatusBarWindow.setExpandAnimationPending(pending);
102         if (pending) {
103             mStatusBarWindow.postDelayed(mTimeoutRunnable, LAUNCH_TIMEOUT);
104         } else {
105             mStatusBarWindow.removeCallbacks(mTimeoutRunnable);
106         }
107     }
108 
109     class AnimationRunner extends IRemoteAnimationRunner.Stub {
110 
111         private final ExpandableNotificationRow mSourceNotification;
112         private final ExpandAnimationParameters mParams;
113         private final Rect mWindowCrop = new Rect();
114         private boolean mInstantCollapsePanel = true;
115         private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
116 
AnimationRunner(ExpandableNotificationRow sourceNofitication)117         public AnimationRunner(ExpandableNotificationRow sourceNofitication) {
118             mSourceNotification = sourceNofitication;
119             mParams = new ExpandAnimationParameters();
120             mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
121         }
122 
123         @Override
onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets, IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)124         public void onAnimationStart(RemoteAnimationTarget[] remoteAnimationTargets,
125                 IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
126                     throws RemoteException {
127             mSourceNotification.post(() -> {
128                 RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
129                         remoteAnimationTargets);
130                 if (primary == null) {
131                     setAnimationPending(false);
132                     invokeCallback(iRemoteAnimationFinishedCallback);
133                     return;
134                 }
135 
136                 setExpandAnimationRunning(true);
137                 mInstantCollapsePanel = primary.position.y == 0
138                         && primary.sourceContainerBounds.height()
139                                 >= mNotificationPanel.getHeight();
140                 if (!mInstantCollapsePanel) {
141                     mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
142                 }
143                 ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
144                 mParams.startPosition = mSourceNotification.getLocationOnScreen();
145                 mParams.startTranslationZ = mSourceNotification.getTranslationZ();
146                 mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
147                 if (mSourceNotification.isChildInGroup()) {
148                     int parentClip = mSourceNotification
149                             .getNotificationParent().getClipTopAmount();
150                     mParams.parentStartClipTopAmount = parentClip;
151                     // We need to calculate how much the child is clipped by the parent
152                     // because children always have 0 clipTopAmount
153                     if (parentClip != 0) {
154                         float childClip = parentClip
155                                 - mSourceNotification.getTranslationY();
156                         if (childClip > 0.0f) {
157                             mParams.startClipTopAmount = (int) Math.ceil(childClip);
158                         }
159                     }
160                 }
161                 int targetWidth = primary.sourceContainerBounds.width();
162                 int notificationHeight = mSourceNotification.getActualHeight()
163                         - mSourceNotification.getClipBottomAmount();
164                 int notificationWidth = mSourceNotification.getWidth();
165                 anim.setDuration(ANIMATION_DURATION);
166                 anim.setInterpolator(Interpolators.LINEAR);
167                 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
168                     @Override
169                     public void onAnimationUpdate(ValueAnimator animation) {
170                         mParams.linearProgress = animation.getAnimatedFraction();
171                         float progress
172                                 = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
173                                         mParams.linearProgress);
174                         int newWidth = (int) MathUtils.lerp(notificationWidth,
175                                 targetWidth, progress);
176                         mParams.left = (int) ((targetWidth - newWidth) / 2.0f);
177                         mParams.right = mParams.left + newWidth;
178                         mParams.top = (int) MathUtils.lerp(mParams.startPosition[1],
179                                 primary.position.y, progress);
180                         mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1]
181                                         + notificationHeight,
182                                 primary.position.y + primary.sourceContainerBounds.bottom,
183                                 progress);
184                         applyParamsToWindow(primary);
185                         applyParamsToNotification(mParams);
186                         applyParamsToNotificationList(mParams);
187                     }
188                 });
189                 anim.addListener(new AnimatorListenerAdapter() {
190                     @Override
191                     public void onAnimationEnd(Animator animation) {
192                         setExpandAnimationRunning(false);
193                         if (mInstantCollapsePanel) {
194                             mStatusBar.collapsePanel(false /* animate */);
195                         }
196                         invokeCallback(iRemoteAnimationFinishedCallback);
197                     }
198                 });
199                 anim.start();
200                 setAnimationPending(false);
201             });
202         }
203 
invokeCallback(IRemoteAnimationFinishedCallback callback)204         private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
205             try {
206                 callback.onAnimationFinished();
207             } catch (RemoteException e) {
208                 e.printStackTrace();
209             }
210         }
211 
getPrimaryRemoteAnimationTarget( RemoteAnimationTarget[] remoteAnimationTargets)212         private RemoteAnimationTarget getPrimaryRemoteAnimationTarget(
213                 RemoteAnimationTarget[] remoteAnimationTargets) {
214             RemoteAnimationTarget primary = null;
215             for (RemoteAnimationTarget app : remoteAnimationTargets) {
216                 if (app.mode == RemoteAnimationTarget.MODE_OPENING) {
217                     primary = app;
218                     break;
219                 }
220             }
221             return primary;
222         }
223 
setExpandAnimationRunning(boolean running)224         private void setExpandAnimationRunning(boolean running) {
225             mNotificationPanel.setLaunchingNotification(running);
226             mSourceNotification.setExpandAnimationRunning(running);
227             mStatusBarWindow.setExpandAnimationRunning(running);
228             mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
229             if (!running) {
230                 applyParamsToNotification(null);
231                 applyParamsToNotificationList(null);
232             }
233 
234         }
235 
applyParamsToNotificationList(ExpandAnimationParameters params)236         private void applyParamsToNotificationList(ExpandAnimationParameters params) {
237             mNotificationContainer.applyExpandAnimationParams(params);
238             mNotificationPanel.applyExpandAnimationParams(params);
239         }
240 
applyParamsToNotification(ExpandAnimationParameters params)241         private void applyParamsToNotification(ExpandAnimationParameters params) {
242             mSourceNotification.applyExpandAnimationParams(params);
243         }
244 
applyParamsToWindow(RemoteAnimationTarget app)245         private void applyParamsToWindow(RemoteAnimationTarget app) {
246             Matrix m = new Matrix();
247             m.postTranslate(0, (float) (mParams.top - app.position.y));
248             mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
249             SurfaceParams params = new SurfaceParams(new SurfaceControlCompat(app.leash),
250                     1f /* alpha */, m, mWindowCrop, app.prefixOrderIndex);
251             mSyncRtTransactionApplier.scheduleApply(params);
252         }
253 
254         @Override
onAnimationCancelled()255         public void onAnimationCancelled() throws RemoteException {
256             mSourceNotification.post(() -> {
257                 setAnimationPending(false);
258                 mStatusBar.onLaunchAnimationCancelled();
259             });
260         }
261     };
262 
263     public static class ExpandAnimationParameters {
264         float linearProgress;
265         int[] startPosition;
266         float startTranslationZ;
267         int left;
268         int top;
269         int right;
270         int bottom;
271         int startClipTopAmount;
272         int parentStartClipTopAmount;
273 
ExpandAnimationParameters()274         public ExpandAnimationParameters() {
275         }
276 
getTop()277         public int getTop() {
278             return top;
279         }
280 
getWidth()281         public int getWidth() {
282             return right - left;
283         }
284 
getHeight()285         public int getHeight() {
286             return bottom - top;
287         }
288 
getTopChange()289         public int getTopChange() {
290             // We need this compensation to ensure that the QS moves in sync.
291             int clipTopAmountCompensation = 0;
292             if (startClipTopAmount != 0.0f) {
293                 clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount,
294                         Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress));
295             }
296             return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0);
297         }
298 
getProgress()299         public float getProgress() {
300             return linearProgress;
301         }
302 
getProgress(long delay, long duration)303         public float getProgress(long delay, long duration) {
304             return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay)
305                     / duration, 0.0f, 1.0f);
306         }
307 
getStartClipTopAmount()308         public int getStartClipTopAmount() {
309             return startClipTopAmount;
310         }
311 
getParentStartClipTopAmount()312         public int getParentStartClipTopAmount() {
313             return parentStartClipTopAmount;
314         }
315 
getStartTranslationZ()316         public float getStartTranslationZ() {
317             return startTranslationZ;
318         }
319     }
320 }
321