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