1 package com.android.systemui.statusbar.notification
2 
3 import android.view.ViewGroup
4 import com.android.internal.jank.InteractionJankMonitor
5 import com.android.systemui.animation.ActivityLaunchAnimator
6 import com.android.systemui.animation.LaunchAnimator
7 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
8 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
9 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
10 import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController
11 import com.android.systemui.statusbar.policy.HeadsUpUtil
12 import kotlin.math.ceil
13 import kotlin.math.max
14 
15 /** A provider of [NotificationLaunchAnimatorController]. */
16 class NotificationLaunchAnimatorControllerProvider(
17     private val notificationShadeWindowViewController: NotificationShadeWindowViewController,
18     private val notificationListContainer: NotificationListContainer,
19     private val headsUpManager: HeadsUpManagerPhone
20 ) {
21     fun getAnimatorController(
22         notification: ExpandableNotificationRow
23     ): NotificationLaunchAnimatorController {
24         return NotificationLaunchAnimatorController(
25             notificationShadeWindowViewController,
26             notificationListContainer,
27             headsUpManager,
28             notification
29         )
30     }
31 }
32 
33 /**
34  * An [ActivityLaunchAnimator.Controller] that animates an [ExpandableNotificationRow]. An instance
35  * of this class can be passed to [ActivityLaunchAnimator.startIntentWithAnimation] to animate a
36  * notification expanding into an opening window.
37  */
38 class NotificationLaunchAnimatorController(
39     private val notificationShadeWindowViewController: NotificationShadeWindowViewController,
40     private val notificationListContainer: NotificationListContainer,
41     private val headsUpManager: HeadsUpManagerPhone,
42     private val notification: ExpandableNotificationRow
43 ) : ActivityLaunchAnimator.Controller {
44 
45     companion object {
46         const val ANIMATION_DURATION_TOP_ROUNDING = 100L
47     }
48 
49     private val notificationEntry = notification.entry
50     private val notificationKey = notificationEntry.sbn.key
51 
52     override var launchContainer: ViewGroup
53         get() = notification.rootView as ViewGroup
54         set(ignored) {
55             // Do nothing. Notifications are always animated inside their rootView.
56         }
57 
58     override fun createAnimatorState(): LaunchAnimator.State {
59         // If the notification panel is collapsed, the clip may be larger than the height.
60         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
61         val location = notification.locationOnScreen
62 
63         val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
64         val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
65         val windowTop = location[1] + roundedTopClipping
66         val topCornerRadius = if (roundedTopClipping > 0) {
67             // Because the rounded Rect clipping is complex, we start the top rounding at
68             // 0, which is pretty close to matching the real clipping.
69             // We'd have to clipOut the overlaid drawable too with the outer rounded rect in case
70             // if we'd like to have this perfect, but this is close enough.
71             0f
72         } else {
73             notification.currentBackgroundRadiusTop
74         }
75         val params = ExpandAnimationParameters(
76             top = windowTop,
77             bottom = location[1] + height,
78             left = location[0],
79             right = location[0] + notification.width,
80             topCornerRadius = topCornerRadius,
81             bottomCornerRadius = notification.currentBackgroundRadiusBottom
82         )
83 
84         params.startTranslationZ = notification.translationZ
85         params.startNotificationTop = notification.translationY
86         params.startRoundedTopClipping = roundedTopClipping
87         params.startClipTopAmount = notification.clipTopAmount
88         if (notification.isChildInGroup) {
89             params.startNotificationTop += notification.notificationParent.translationY
90             val parentRoundedClip = Math.max(
91                 clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
92             params.parentStartRoundedTopClipping = parentRoundedClip
93 
94             val parentClip = notification.notificationParent.clipTopAmount
95             params.parentStartClipTopAmount = parentClip
96 
97             // We need to calculate how much the child is clipped by the parent because children
98             // always have 0 clipTopAmount
99             if (parentClip != 0) {
100                 val childClip = parentClip - notification.translationY
101                 if (childClip > 0) {
102                     params.startClipTopAmount = ceil(childClip.toDouble()).toInt()
103                 }
104             }
105         }
106 
107         return params
108     }
109 
110     override fun onIntentStarted(willAnimate: Boolean) {
111         notificationShadeWindowViewController.setExpandAnimationRunning(willAnimate)
112         notificationEntry.isExpandAnimationRunning = willAnimate
113 
114         if (!willAnimate) {
115             removeHun(animate = true)
116         }
117     }
118 
119     private fun removeHun(animate: Boolean) {
120         if (!headsUpManager.isAlerting(notificationKey)) {
121             return
122         }
123 
124         HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate)
125         headsUpManager.removeNotification(notificationKey, true /* releaseImmediately */, animate)
126     }
127 
128     override fun onLaunchAnimationCancelled() {
129         // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started
130         // here?
131         notificationShadeWindowViewController.setExpandAnimationRunning(false)
132         notificationEntry.isExpandAnimationRunning = false
133         removeHun(animate = true)
134     }
135 
136     override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
137         notification.isExpandAnimationRunning = true
138         notificationListContainer.setExpandingNotification(notification)
139 
140         InteractionJankMonitor.getInstance().begin(notification,
141             InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
142     }
143 
144     override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
145         InteractionJankMonitor.getInstance().end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
146 
147         notification.isExpandAnimationRunning = false
148         notificationShadeWindowViewController.setExpandAnimationRunning(false)
149         notificationEntry.isExpandAnimationRunning = false
150         notificationListContainer.setExpandingNotification(null)
151         applyParams(null)
152         removeHun(animate = false)
153     }
154 
155     private fun applyParams(params: ExpandAnimationParameters?) {
156         notification.applyExpandAnimationParams(params)
157         notificationListContainer.applyExpandAnimationParams(params)
158     }
159 
160     override fun onLaunchAnimationProgress(
161         state: LaunchAnimator.State,
162         progress: Float,
163         linearProgress: Float
164     ) {
165         val params = state as ExpandAnimationParameters
166         params.progress = progress
167         params.linearProgress = linearProgress
168 
169         applyParams(params)
170     }
171 }
172