1 /*
2  * Copyright (C) 2021 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.util.Log
20 import android.view.ViewGroup
21 import com.android.internal.jank.InteractionJankMonitor
22 import com.android.systemui.animation.ActivityTransitionAnimator
23 import com.android.systemui.animation.TransitionAnimator
24 import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
25 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
26 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
27 import com.android.systemui.statusbar.policy.HeadsUpManager
28 import com.android.systemui.statusbar.policy.HeadsUpUtil
29 import kotlin.math.ceil
30 import kotlin.math.max
31 
32 private const val TAG = "NotificationLaunchAnimatorController"
33 
34 /** A provider of [NotificationTransitionAnimatorController]. */
35 class NotificationLaunchAnimatorControllerProvider(
36     private val notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor,
37     private val notificationListContainer: NotificationListContainer,
38     private val headsUpManager: HeadsUpManager,
39     private val jankMonitor: InteractionJankMonitor
40 ) {
41     @JvmOverloads
getAnimatorControllernull42     fun getAnimatorController(
43         notification: ExpandableNotificationRow,
44         onFinishAnimationCallback: Runnable? = null
45     ): NotificationTransitionAnimatorController {
46         return NotificationTransitionAnimatorController(
47             notificationLaunchAnimationInteractor,
48             notificationListContainer,
49             headsUpManager,
50             notification,
51             jankMonitor,
52             onFinishAnimationCallback
53         )
54     }
55 }
56 
57 /**
58  * An [ActivityTransitionAnimator.Controller] that animates an [ExpandableNotificationRow]. An
59  * instance of this class can be passed to [ActivityTransitionAnimator.startIntentWithAnimation] to
60  * animate a notification expanding into an opening window.
61  */
62 class NotificationTransitionAnimatorController(
63     private val notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor,
64     private val notificationListContainer: NotificationListContainer,
65     private val headsUpManager: HeadsUpManager,
66     private val notification: ExpandableNotificationRow,
67     private val jankMonitor: InteractionJankMonitor,
68     private val onFinishAnimationCallback: Runnable?
69 ) : ActivityTransitionAnimator.Controller {
70 
71     companion object {
72         const val ANIMATION_DURATION_TOP_ROUNDING = 100L
73     }
74 
75     private val notificationEntry = notification.entry
76     private val notificationKey = notificationEntry.sbn.key
77 
78     override val isLaunching: Boolean = true
79 
80     override var transitionContainer: ViewGroup
81         get() = notification.rootView as ViewGroup
82         set(ignored) {
83             // Do nothing. Notifications are always animated inside their rootView.
84         }
85 
createAnimatorStatenull86     override fun createAnimatorState(): TransitionAnimator.State {
87         // If the notification panel is collapsed, the clip may be larger than the height.
88         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
89         val location = notification.locationOnScreen
90 
91         val clipStartLocation = notificationListContainer.topClippingStartLocation
92         val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
93         val windowTop = location[1] + roundedTopClipping
94         val topCornerRadius =
95             if (roundedTopClipping > 0) {
96                 // Because the rounded Rect clipping is complex, we start the top rounding at
97                 // 0, which is pretty close to matching the real clipping.
98                 // We'd have to clipOut the overlaid drawable too with the outer rounded rect in
99                 // case
100                 // if we'd like to have this perfect, but this is close enough.
101                 0f
102             } else {
103                 notification.topCornerRadius
104             }
105         val params =
106             LaunchAnimationParameters(
107                 top = windowTop,
108                 bottom = location[1] + height,
109                 left = location[0],
110                 right = location[0] + notification.width,
111                 topCornerRadius = topCornerRadius,
112                 bottomCornerRadius = notification.bottomCornerRadius
113             )
114 
115         params.startTranslationZ = notification.translationZ
116         params.startNotificationTop = location[1]
117         params.notificationParentTop =
118             notificationListContainer
119                 .getViewParentForNotification(notificationEntry)
120                 .locationOnScreen[1]
121         params.startRoundedTopClipping = roundedTopClipping
122         params.startClipTopAmount = notification.clipTopAmount
123         if (notification.isChildInGroup) {
124             val locationOnScreen = notification.notificationParent.locationOnScreen[1]
125             val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
126             params.parentStartRoundedTopClipping = parentRoundedClip
127 
128             val parentClip = notification.notificationParent.clipTopAmount
129             params.parentStartClipTopAmount = parentClip
130 
131             // We need to calculate how much the child is clipped by the parent because children
132             // always have 0 clipTopAmount
133             if (parentClip != 0) {
134                 val childClip = parentClip - notification.translationY
135                 if (childClip > 0) {
136                     params.startClipTopAmount = ceil(childClip.toDouble()).toInt()
137                 }
138             }
139         }
140 
141         return params
142     }
143 
onIntentStartednull144     override fun onIntentStarted(willAnimate: Boolean) {
145         if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
146             Log.d(TAG, "onIntentStarted(willAnimate=$willAnimate)")
147         }
148         notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(willAnimate)
149         notificationEntry.isExpandAnimationRunning = willAnimate
150 
151         if (!willAnimate) {
152             removeHun(animate = true)
153             onFinishAnimationCallback?.run()
154         }
155     }
156 
157     private val headsUpNotificationRow: ExpandableNotificationRow?
158         get() {
159             val summaryEntry = notificationEntry.parent?.summary
160 
161             return when {
162                 headsUpManager.isHeadsUpEntry(notificationKey) -> notification
163                 summaryEntry == null -> null
164                 headsUpManager.isHeadsUpEntry(summaryEntry.key) -> summaryEntry.row
165                 else -> null
166             }
167         }
168 
removeHunnull169     private fun removeHun(animate: Boolean) {
170         val row = headsUpNotificationRow ?: return
171 
172         // TODO: b/297247841 - Call on the row we're removing, which may differ from notification.
173         HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate)
174 
175         headsUpManager.removeNotification(row.entry.key, true /* releaseImmediately */, animate)
176     }
177 
onTransitionAnimationCancellednull178     override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
179         if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
180             Log.d(TAG, "onLaunchAnimationCancelled()")
181         }
182 
183         // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started
184         // here?
185         notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
186         notificationEntry.isExpandAnimationRunning = false
187         removeHun(animate = true)
188         onFinishAnimationCallback?.run()
189     }
190 
onTransitionAnimationStartnull191     override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
192         notification.isExpandAnimationRunning = true
193         notificationListContainer.setExpandingNotification(notification)
194 
195         jankMonitor.begin(notification, InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
196     }
197 
onTransitionAnimationEndnull198     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
199         if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
200             Log.d(TAG, "onLaunchAnimationEnd()")
201         }
202         jankMonitor.end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
203 
204         notification.isExpandAnimationRunning = false
205         notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
206         notificationEntry.isExpandAnimationRunning = false
207         notificationListContainer.setExpandingNotification(null)
208         applyParams(null)
209         removeHun(animate = false)
210         onFinishAnimationCallback?.run()
211     }
212 
applyParamsnull213     private fun applyParams(params: LaunchAnimationParameters?) {
214         notification.applyLaunchAnimationParams(params)
215         notificationListContainer.applyLaunchAnimationParams(params)
216     }
217 
onTransitionAnimationProgressnull218     override fun onTransitionAnimationProgress(
219         state: TransitionAnimator.State,
220         progress: Float,
221         linearProgress: Float
222     ) {
223         val params = state as LaunchAnimationParameters
224         params.progress = progress
225         params.linearProgress = linearProgress
226 
227         applyParams(params)
228     }
229 }
230