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