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