1 /* 2 * Copyright (C) 2019 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.ObjectAnimator 20 import android.util.FloatProperty 21 import com.android.systemui.Interpolators 22 import com.android.systemui.plugins.statusbar.StatusBarStateController 23 import com.android.systemui.statusbar.StatusBarState 24 import com.android.systemui.statusbar.notification.collection.NotificationEntry 25 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout 26 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 27 import com.android.systemui.statusbar.phone.DozeParameters 28 import com.android.systemui.statusbar.phone.KeyguardBypassController 29 import com.android.systemui.statusbar.phone.NotificationIconAreaController 30 import com.android.systemui.statusbar.phone.PanelExpansionListener 31 import com.android.systemui.statusbar.policy.HeadsUpManager 32 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener 33 34 import javax.inject.Inject 35 import javax.inject.Singleton 36 37 @Singleton 38 class NotificationWakeUpCoordinator @Inject constructor( 39 private val mHeadsUpManager: HeadsUpManager, 40 private val statusBarStateController: StatusBarStateController, 41 private val bypassController: KeyguardBypassController, 42 private val dozeParameters: DozeParameters 43 ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener { 44 45 private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( 46 "notificationVisibility") { 47 setValuenull48 override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) { 49 coordinator.setVisibilityAmount(value) 50 } 51 getnull52 override fun get(coordinator: NotificationWakeUpCoordinator): Float? { 53 return coordinator.mLinearVisibilityAmount 54 } 55 } 56 private lateinit var mStackScroller: NotificationStackScrollLayout 57 private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE 58 59 private var mLinearDozeAmount: Float = 0.0f 60 private var mDozeAmount: Float = 0.0f 61 private var mNotificationVisibleAmount = 0.0f 62 private var mNotificationsVisible = false 63 private var mNotificationsVisibleForExpansion = false 64 private var mVisibilityAnimator: ObjectAnimator? = null 65 private var mVisibilityAmount = 0.0f 66 private var mLinearVisibilityAmount = 0.0f 67 private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>() 68 private var pulseExpanding: Boolean = false 69 private val wakeUpListeners = arrayListOf<WakeUpListener>() 70 private var state: Int = StatusBarState.KEYGUARD 71 72 var fullyAwake: Boolean = false 73 74 var wakingUp = false 75 set(value) { 76 field = value 77 willWakeUp = false 78 if (value) { 79 if (mNotificationsVisible && !mNotificationsVisibleForExpansion && 80 !bypassController.bypassEnabled) { 81 // We're waking up while pulsing, let's make sure the animation looks nice 82 mStackScroller.wakeUpFromPulse() 83 } 84 if (bypassController.bypassEnabled && !mNotificationsVisible) { 85 // Let's make sure our huns become visible once we are waking up in case 86 // they were blocked by the proximity sensor 87 updateNotificationVisibility(animate = shouldAnimateVisibility(), 88 increaseSpeed = false) 89 } 90 } 91 } 92 93 var willWakeUp = false 94 set(value) { 95 if (!value || mDozeAmount != 0.0f) { 96 field = value 97 } 98 } 99 100 private var collapsedEnoughToHide: Boolean = false 101 lateinit var iconAreaController: NotificationIconAreaController 102 103 var pulsing: Boolean = false 104 set(value) { 105 field = value 106 if (value) { 107 // Only when setting pulsing to true we want an immediate update, since we get 108 // this already when the doze service finishes which is usually before we get 109 // the waking up callback 110 updateNotificationVisibility(animate = shouldAnimateVisibility(), 111 increaseSpeed = false) 112 } 113 } 114 115 var notificationsFullyHidden: Boolean = false 116 private set(value) { 117 if (field != value) { 118 field = value 119 for (listener in wakeUpListeners) { 120 listener.onFullyHiddenChanged(value) 121 } 122 } 123 } 124 /** 125 * True if we can show pulsing heads up notifications 126 */ 127 var canShowPulsingHuns: Boolean = false 128 private set 129 get() { 130 var canShow = pulsing 131 if (bypassController.bypassEnabled) { 132 // We also allow pulsing on the lock screen! 133 canShow = canShow || (wakingUp || willWakeUp || fullyAwake) && 134 statusBarStateController.state == StatusBarState.KEYGUARD 135 // We want to hide the notifications when collapsed too much 136 if (collapsedEnoughToHide) { 137 canShow = false 138 } 139 } 140 return canShow 141 } 142 143 init { 144 mHeadsUpManager.addListener(this) 145 statusBarStateController.addCallback(this) 146 addListener(object : WakeUpListener { onFullyHiddenChangednull147 override fun onFullyHiddenChanged(isFullyHidden: Boolean) { 148 if (isFullyHidden && mNotificationsVisibleForExpansion) { 149 // When the notification becomes fully invisible, let's make sure our expansion 150 // flag also changes. This can happen if the bouncer shows when dragging down 151 // and then the screen turning off, where we don't reset this state. 152 setNotificationsVisibleForExpansion(visible = false, animate = false, 153 increaseSpeed = false) 154 } 155 } 156 }) 157 } 158 setStackScrollernull159 fun setStackScroller(stackScroller: NotificationStackScrollLayout) { 160 mStackScroller = stackScroller 161 pulseExpanding = stackScroller.isPulseExpanding 162 stackScroller.setOnPulseHeightChangedListener { 163 val nowExpanding = isPulseExpanding() 164 val changed = nowExpanding != pulseExpanding 165 pulseExpanding = nowExpanding 166 for (listener in wakeUpListeners) { 167 listener.onPulseExpansionChanged(changed) 168 } 169 } 170 } 171 isPulseExpandingnull172 fun isPulseExpanding(): Boolean = mStackScroller.isPulseExpanding 173 174 /** 175 * @param visible should notifications be visible 176 * @param animate should this change be animated 177 * @param increaseSpeed should the speed be increased of the animation 178 */ 179 fun setNotificationsVisibleForExpansion( 180 visible: Boolean, 181 animate: Boolean, 182 increaseSpeed: Boolean 183 ) { 184 mNotificationsVisibleForExpansion = visible 185 updateNotificationVisibility(animate, increaseSpeed) 186 if (!visible && mNotificationsVisible) { 187 // If we stopped expanding and we're still visible because we had a pulse that hasn't 188 // times out, let's release them all to make sure were not stuck in a state where 189 // notifications are visible 190 mHeadsUpManager.releaseAllImmediately() 191 } 192 } 193 addListenernull194 fun addListener(listener: WakeUpListener) { 195 wakeUpListeners.add(listener) 196 } 197 removeListenernull198 fun removeListener(listener: WakeUpListener) { 199 wakeUpListeners.remove(listener) 200 } 201 updateNotificationVisibilitynull202 private fun updateNotificationVisibility( 203 animate: Boolean, 204 increaseSpeed: Boolean 205 ) { 206 // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore 207 var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications() 208 visible = visible && canShowPulsingHuns 209 210 if (!visible && mNotificationsVisible && (wakingUp || willWakeUp) && mDozeAmount != 0.0f) { 211 // let's not make notifications invisible while waking up, otherwise the animation 212 // is strange 213 return 214 } 215 setNotificationsVisible(visible, animate, increaseSpeed) 216 } 217 setNotificationsVisiblenull218 private fun setNotificationsVisible( 219 visible: Boolean, 220 animate: Boolean, 221 increaseSpeed: Boolean 222 ) { 223 if (mNotificationsVisible == visible) { 224 return 225 } 226 mNotificationsVisible = visible 227 mVisibilityAnimator?.cancel() 228 if (animate) { 229 notifyAnimationStart(visible) 230 startVisibilityAnimation(increaseSpeed) 231 } else { 232 setVisibilityAmount(if (visible) 1.0f else 0.0f) 233 } 234 } 235 onDozeAmountChangednull236 override fun onDozeAmountChanged(linear: Float, eased: Float) { 237 if (updateDozeAmountIfBypass()) { 238 return 239 } 240 if (linear != 1.0f && linear != 0.0f && 241 (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) { 242 // Let's notify the scroller that an animation started 243 notifyAnimationStart(mLinearDozeAmount == 1.0f) 244 } 245 setDozeAmount(linear, eased) 246 } 247 setDozeAmountnull248 fun setDozeAmount(linear: Float, eased: Float) { 249 val changed = linear != mLinearDozeAmount 250 mLinearDozeAmount = linear 251 mDozeAmount = eased 252 mStackScroller.setDozeAmount(mDozeAmount) 253 updateHideAmount() 254 if (changed && linear == 0.0f) { 255 setNotificationsVisible(visible = false, animate = false, increaseSpeed = false) 256 setNotificationsVisibleForExpansion(visible = false, animate = false, 257 increaseSpeed = false) 258 } 259 } 260 onStateChangednull261 override fun onStateChanged(newState: Int) { 262 updateDozeAmountIfBypass() 263 if (bypassController.bypassEnabled && 264 newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED && 265 (!statusBarStateController.isDozing || shouldAnimateVisibility())) { 266 // We're leaving shade locked. Let's animate the notifications away 267 setNotificationsVisible(visible = true, increaseSpeed = false, animate = false) 268 setNotificationsVisible(visible = false, increaseSpeed = false, animate = true) 269 } 270 this.state = newState 271 } 272 onPanelExpansionChangednull273 override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) { 274 val collapsedEnough = expansion <= 0.9f 275 if (collapsedEnough != this.collapsedEnoughToHide) { 276 val couldShowPulsingHuns = canShowPulsingHuns 277 this.collapsedEnoughToHide = collapsedEnough 278 if (couldShowPulsingHuns && !canShowPulsingHuns) { 279 updateNotificationVisibility(animate = true, increaseSpeed = true) 280 mHeadsUpManager.releaseAllImmediately() 281 } 282 } 283 } 284 updateDozeAmountIfBypassnull285 private fun updateDozeAmountIfBypass(): Boolean { 286 if (bypassController.bypassEnabled) { 287 var amount = 1.0f 288 if (statusBarStateController.state == StatusBarState.SHADE || 289 statusBarStateController.state == StatusBarState.SHADE_LOCKED) { 290 amount = 0.0f 291 } 292 setDozeAmount(amount, amount) 293 return true 294 } 295 return false 296 } 297 startVisibilityAnimationnull298 private fun startVisibilityAnimation(increaseSpeed: Boolean) { 299 if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) { 300 mVisibilityInterpolator = if (mNotificationsVisible) 301 Interpolators.TOUCH_RESPONSE 302 else 303 Interpolators.FAST_OUT_SLOW_IN_REVERSE 304 } 305 val target = if (mNotificationsVisible) 1.0f else 0.0f 306 val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target) 307 visibilityAnimator.setInterpolator(Interpolators.LINEAR) 308 var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong() 309 if (increaseSpeed) { 310 duration = (duration.toFloat() / 1.5F).toLong() 311 } 312 visibilityAnimator.setDuration(duration) 313 visibilityAnimator.start() 314 mVisibilityAnimator = visibilityAnimator 315 } 316 setVisibilityAmountnull317 private fun setVisibilityAmount(visibilityAmount: Float) { 318 mLinearVisibilityAmount = visibilityAmount 319 mVisibilityAmount = mVisibilityInterpolator.getInterpolation( 320 visibilityAmount) 321 handleAnimationFinished() 322 updateHideAmount() 323 } 324 handleAnimationFinishednull325 private fun handleAnimationFinished() { 326 if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) { 327 mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) } 328 mEntrySetToClearWhenFinished.clear() 329 } 330 } 331 getWakeUpHeightnull332 fun getWakeUpHeight(): Float { 333 return mStackScroller.wakeUpHeight 334 } 335 updateHideAmountnull336 private fun updateHideAmount() { 337 val linearAmount = Math.min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount) 338 val amount = Math.min(1.0f - mVisibilityAmount, mDozeAmount) 339 mStackScroller.setHideAmount(linearAmount, amount) 340 notificationsFullyHidden = linearAmount == 1.0f 341 } 342 notifyAnimationStartnull343 private fun notifyAnimationStart(awake: Boolean) { 344 mStackScroller.notifyHideAnimationStart(!awake) 345 } 346 onDozingChangednull347 override fun onDozingChanged(isDozing: Boolean) { 348 if (isDozing) { 349 setNotificationsVisible(visible = false, animate = false, increaseSpeed = false) 350 } 351 } 352 353 /** 354 * Set the height how tall notifications are pulsing. This is only set whenever we are expanding 355 * from a pulse and determines how much the notifications are expanded. 356 */ setPulseHeightnull357 fun setPulseHeight(height: Float): Float { 358 val overflow = mStackScroller.setPulseHeight(height) 359 // no overflow for the bypass experience 360 return if (bypassController.bypassEnabled) 0.0f else overflow 361 } 362 onHeadsUpStateChangednull363 override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { 364 var animate = shouldAnimateVisibility() 365 if (!isHeadsUp) { 366 if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) { 367 if (entry.isRowDismissed) { 368 // if we animate, we see the shelf briefly visible. Instead we fully animate 369 // the notification and its background out 370 animate = false 371 } else if (!wakingUp && !willWakeUp) { 372 // TODO: look that this is done properly and not by anyone else 373 entry.setHeadsUpAnimatingAway(true) 374 mEntrySetToClearWhenFinished.add(entry) 375 } 376 } 377 } else if (mEntrySetToClearWhenFinished.contains(entry)) { 378 mEntrySetToClearWhenFinished.remove(entry) 379 entry.setHeadsUpAnimatingAway(false) 380 } 381 updateNotificationVisibility(animate, increaseSpeed = false) 382 } 383 shouldAnimateVisibilitynull384 private fun shouldAnimateVisibility() = 385 dozeParameters.getAlwaysOn() && !dozeParameters.getDisplayNeedsBlanking() 386 387 interface WakeUpListener { 388 /** 389 * Called whenever the notifications are fully hidden or shown 390 */ 391 @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {} 392 393 /** 394 * Called whenever the pulseExpansion changes 395 * @param expandingChanged if the user has started or stopped expanding 396 */ 397 @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {} 398 } 399 }