<lambda>null1 package com.android.systemui.statusbar
2
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.ValueAnimator
6 import android.content.Context
7 import android.content.res.Configuration
8 import android.os.PowerManager
9 import android.os.SystemClock
10 import android.util.IndentingPrintWriter
11 import android.util.MathUtils
12 import android.view.MotionEvent
13 import android.view.View
14 import android.view.ViewConfiguration
15 import androidx.annotation.FloatRange
16 import androidx.annotation.VisibleForTesting
17 import com.android.systemui.Dumpable
18 import com.android.systemui.ExpandHelper
19 import com.android.systemui.Gefingerpoken
20 import com.android.systemui.R
21 import com.android.systemui.animation.Interpolators
22 import com.android.systemui.biometrics.UdfpsKeyguardViewController
23 import com.android.systemui.classifier.Classifier
24 import com.android.systemui.classifier.FalsingCollector
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dump.DumpManager
27 import com.android.systemui.keyguard.WakefulnessLifecycle
28 import com.android.systemui.media.controls.ui.MediaHierarchyManager
29 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
30 import com.android.systemui.plugins.FalsingManager
31 import com.android.systemui.plugins.qs.QS
32 import com.android.systemui.plugins.statusbar.StatusBarStateController
33 import com.android.systemui.shade.NotificationPanelViewController
34 import com.android.systemui.statusbar.notification.collection.NotificationEntry
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
36 import com.android.systemui.statusbar.notification.row.ExpandableView
37 import com.android.systemui.statusbar.notification.stack.AmbientState
38 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
39 import com.android.systemui.statusbar.phone.CentralSurfaces
40 import com.android.systemui.statusbar.phone.KeyguardBypassController
41 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
42 import com.android.systemui.statusbar.policy.ConfigurationController
43 import com.android.systemui.util.LargeScreenUtils
44 import java.io.PrintWriter
45 import javax.inject.Inject
46
47 private const val SPRING_BACK_ANIMATION_LENGTH_MS = 375L
48 private const val RUBBERBAND_FACTOR_STATIC = 0.15f
49 private const val RUBBERBAND_FACTOR_EXPANDABLE = 0.5f
50
51 /**
52 * A class that controls the lockscreen to shade transition
53 */
54 @SysUISingleton
55 class LockscreenShadeTransitionController @Inject constructor(
56 private val statusBarStateController: SysuiStatusBarStateController,
57 private val logger: LSShadeTransitionLogger,
58 private val keyguardBypassController: KeyguardBypassController,
59 private val lockScreenUserManager: NotificationLockscreenUserManager,
60 private val falsingCollector: FalsingCollector,
61 private val ambientState: AmbientState,
62 private val mediaHierarchyManager: MediaHierarchyManager,
63 private val scrimTransitionController: LockscreenShadeScrimTransitionController,
64 private val keyguardTransitionControllerFactory:
65 LockscreenShadeKeyguardTransitionController.Factory,
66 private val depthController: NotificationShadeDepthController,
67 private val context: Context,
68 private val splitShadeOverScrollerFactory: SplitShadeLockScreenOverScroller.Factory,
69 private val singleShadeOverScrollerFactory: SingleShadeLockScreenOverScroller.Factory,
70 wakefulnessLifecycle: WakefulnessLifecycle,
71 configurationController: ConfigurationController,
72 falsingManager: FalsingManager,
73 dumpManager: DumpManager,
74 qsTransitionControllerFactory: LockscreenShadeQsTransitionController.Factory,
75 ) : Dumpable {
76 private var pulseHeight: Float = 0f
77 @get:VisibleForTesting
78 var fractionToShade: Float = 0f
79 private set
80 private var useSplitShade: Boolean = false
81 private lateinit var nsslController: NotificationStackScrollLayoutController
82 lateinit var notificationPanelController: NotificationPanelViewController
83 lateinit var centralSurfaces: CentralSurfaces
84 lateinit var qS: QS
85
86 /**
87 * A handler that handles the next keyguard dismiss animation.
88 */
89 private var animationHandlerOnKeyguardDismiss: ((Long) -> Unit)? = null
90
91 /**
92 * The entry that was just dragged down on.
93 */
94 private var draggedDownEntry: NotificationEntry? = null
95
96 /**
97 * The current animator if any
98 */
99 @VisibleForTesting
100 internal var dragDownAnimator: ValueAnimator? = null
101
102 /**
103 * The current pulse height animator if any
104 */
105 @VisibleForTesting
106 internal var pulseHeightAnimator: ValueAnimator? = null
107
108 /**
109 * Distance that the full shade transition takes in order to complete.
110 */
111 private var fullTransitionDistance = 0
112
113 /**
114 * Distance that the full transition takes in order for us to fully transition to the shade by
115 * tapping on a button, such as "expand".
116 */
117 private var fullTransitionDistanceByTap = 0
118
119 /**
120 * Distance that the full shade transition takes in order for the notification shelf to fully
121 * expand.
122 */
123 private var notificationShelfTransitionDistance = 0
124
125 /**
126 * Distance that the full shade transition takes in order for depth of the wallpaper to fully
127 * change.
128 */
129 private var depthControllerTransitionDistance = 0
130
131 /**
132 * Distance that the full shade transition takes in order for the UDFPS Keyguard View to fully
133 * fade.
134 */
135 private var udfpsTransitionDistance = 0
136
137 /**
138 * Used for StatusBar to know that a transition is in progress. At the moment it only checks
139 * whether the progress is > 0, therefore this value is not very important.
140 */
141 private var statusBarTransitionDistance = 0
142
143 /**
144 * Flag to make sure that the dragDownAmount is applied to the listeners even when in the
145 * locked down shade.
146 */
147 private var forceApplyAmount = false
148
149 /**
150 * A flag to suppress the default animation when unlocking in the locked down shade.
151 */
152 private var nextHideKeyguardNeedsNoAnimation = false
153
154 /**
155 * Are we currently waking up to the shade locked
156 */
157 var isWakingToShadeLocked: Boolean = false
158 private set
159
160 /**
161 * The distance until we're showing the notifications when pulsing
162 */
163 val distanceUntilShowingPulsingNotifications
164 get() = fullTransitionDistance
165
166 /**
167 * The udfpsKeyguardViewController if it exists.
168 */
169 var udfpsKeyguardViewController: UdfpsKeyguardViewController? = null
170
171 /**
172 * The touch helper responsible for the drag down animation.
173 */
174 val touchHelper = DragDownHelper(falsingManager, falsingCollector, this, context)
175
176 private val splitShadeOverScroller: SplitShadeLockScreenOverScroller by lazy {
177 splitShadeOverScrollerFactory.create({ qS }, { nsslController })
178 }
179
180 private val phoneShadeOverScroller: SingleShadeLockScreenOverScroller by lazy {
181 singleShadeOverScrollerFactory.create(nsslController)
182 }
183
184 private val keyguardTransitionController by lazy {
185 keyguardTransitionControllerFactory.create(notificationPanelController)
186 }
187
188 private val qsTransitionController = qsTransitionControllerFactory.create { qS }
189
190 private val callbacks = mutableListOf<Callback>()
191
192 /** See [LockscreenShadeQsTransitionController.qsTransitionFraction].*/
193 @get:FloatRange(from = 0.0, to = 1.0)
194 val qSDragProgress: Float
195 get() = qsTransitionController.qsTransitionFraction
196
197 /** See [LockscreenShadeQsTransitionController.qsSquishTransitionFraction].*/
198 @get:FloatRange(from = 0.0, to = 1.0)
199 val qsSquishTransitionFraction: Float
200 get() = qsTransitionController.qsSquishTransitionFraction
201
202 /**
203 * [LockScreenShadeOverScroller] property that delegates to either
204 * [SingleShadeLockScreenOverScroller] or [SplitShadeLockScreenOverScroller].
205 *
206 * There are currently two different implementations, as the over scroll behavior is different
207 * on single shade and split shade.
208 *
209 * On single shade, only notifications are over scrolled, whereas on split shade, everything is
210 * over scrolled.
211 */
212 private val shadeOverScroller: LockScreenShadeOverScroller
213 get() = if (useSplitShade) splitShadeOverScroller else phoneShadeOverScroller
214
215 init {
216 updateResources()
217 configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
218 override fun onConfigChanged(newConfig: Configuration?) {
219 updateResources()
220 touchHelper.updateResources(context)
221 }
222 })
223 dumpManager.registerDumpable(this)
224 statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
225 override fun onExpandedChanged(isExpanded: Boolean) {
226 // safeguard: When the panel is fully collapsed, let's make sure to reset.
227 // See b/198098523
228 if (!isExpanded) {
229 if (dragDownAmount != 0f && dragDownAnimator?.isRunning != true) {
230 logger.logDragDownAmountResetWhenFullyCollapsed()
231 dragDownAmount = 0f
232 }
233 if (pulseHeight != 0f && pulseHeightAnimator?.isRunning != true) {
234 logger.logPulseHeightNotResetWhenFullyCollapsed()
235 setPulseHeight(0f, animate = false)
236 }
237 }
238 }
239 })
240 wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
241 override fun onPostFinishedWakingUp() {
242 // when finishing waking up, the UnlockedScreenOffAnimation has another attempt
243 // to reset keyguard. Let's do it in post
244 isWakingToShadeLocked = false
245 }
246 })
247 }
248
249 private fun updateResources() {
250 fullTransitionDistance = context.resources.getDimensionPixelSize(
251 R.dimen.lockscreen_shade_full_transition_distance)
252 fullTransitionDistanceByTap = context.resources.getDimensionPixelSize(
253 R.dimen.lockscreen_shade_transition_by_tap_distance)
254 notificationShelfTransitionDistance = context.resources.getDimensionPixelSize(
255 R.dimen.lockscreen_shade_notif_shelf_transition_distance)
256 depthControllerTransitionDistance = context.resources.getDimensionPixelSize(
257 R.dimen.lockscreen_shade_depth_controller_transition_distance)
258 udfpsTransitionDistance = context.resources.getDimensionPixelSize(
259 R.dimen.lockscreen_shade_udfps_keyguard_transition_distance)
260 statusBarTransitionDistance = context.resources.getDimensionPixelSize(
261 R.dimen.lockscreen_shade_status_bar_transition_distance)
262 useSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
263 }
264
265 fun setStackScroller(nsslController: NotificationStackScrollLayoutController) {
266 this.nsslController = nsslController
267 touchHelper.expandCallback = nsslController.expandHelperCallback
268 }
269
270 /**
271 * Initialize the shelf controller such that clicks on it will expand the shade
272 */
273 fun bindController(notificationShelfController: NotificationShelfController) {
274 // Bind the click listener of the shelf to go to the full shade
275 notificationShelfController.setOnClickListener {
276 if (statusBarStateController.state == StatusBarState.KEYGUARD) {
277 centralSurfaces.wakeUpIfDozing(
278 SystemClock.uptimeMillis(),
279 it,
280 "SHADE_CLICK",
281 PowerManager.WAKE_REASON_GESTURE,
282 )
283 goToLockedShade(it)
284 }
285 }
286 }
287
288 /**
289 * @return true if the interaction is accepted, false if it should be cancelled
290 */
291 internal fun canDragDown(): Boolean {
292 return (statusBarStateController.state == StatusBarState.KEYGUARD ||
293 nsslController.isInLockedDownShade()) &&
294 (qS.isFullyCollapsed || useSplitShade)
295 }
296
297 /**
298 * Called by the touch helper when when a gesture has completed all the way and released.
299 */
300 internal fun onDraggedDown(startingChild: View?, dragLengthY: Int) {
301 if (canDragDown()) {
302 val cancelRunnable = Runnable {
303 logger.logGoingToLockedShadeAborted()
304 setDragDownAmountAnimated(0f)
305 }
306 if (nsslController.isInLockedDownShade()) {
307 logger.logDraggedDownLockDownShade(startingChild)
308 statusBarStateController.setLeaveOpenOnKeyguardHide(true)
309 centralSurfaces.dismissKeyguardThenExecute(OnDismissAction {
310 nextHideKeyguardNeedsNoAnimation = true
311 false
312 }, cancelRunnable, false /* afterKeyguardGone */)
313 } else {
314 logger.logDraggedDown(startingChild, dragLengthY)
315 if (!ambientState.isDozing() || startingChild != null) {
316 // go to locked shade while animating the drag down amount from its current
317 // value
318 val animationHandler = { delay: Long ->
319 if (startingChild is ExpandableNotificationRow) {
320 startingChild.onExpandedByGesture(
321 true /* drag down is always an open */)
322 }
323 notificationPanelController.animateToFullShade(delay)
324 callbacks.forEach { it.setTransitionToFullShadeAmount(0f,
325 true /* animated */, delay) }
326
327 // Let's reset ourselves, ready for the next animation
328
329 // changing to shade locked will make isInLockDownShade true, so let's
330 // override that
331 forceApplyAmount = true
332 // Reset the behavior. At this point the animation is already started
333 logger.logDragDownAmountReset()
334 dragDownAmount = 0f
335 forceApplyAmount = false
336 }
337 goToLockedShadeInternal(startingChild, animationHandler, cancelRunnable)
338 }
339 }
340 } else {
341 logger.logUnSuccessfulDragDown(startingChild)
342 setDragDownAmountAnimated(0f)
343 }
344 }
345
346 /**
347 * Called by the touch helper when the drag down was aborted and should be reset.
348 */
349 internal fun onDragDownReset() {
350 logger.logDragDownAborted()
351 nsslController.setDimmed(true /* dimmed */, true /* animated */)
352 nsslController.resetScrollPosition()
353 nsslController.resetCheckSnoozeLeavebehind()
354 setDragDownAmountAnimated(0f)
355 }
356
357 /**
358 * The user has dragged either above or below the threshold which changes the dimmed state.
359 * @param above whether they dragged above it
360 */
361 internal fun onCrossedThreshold(above: Boolean) {
362 nsslController.setDimmed(!above /* dimmed */, true /* animate */)
363 }
364
365 /**
366 * Called by the touch helper when the drag down was started
367 */
368 internal fun onDragDownStarted(startingChild: ExpandableView?) {
369 logger.logDragDownStarted(startingChild)
370 nsslController.cancelLongPress()
371 nsslController.checkSnoozeLeavebehind()
372 dragDownAnimator?.apply {
373 if (isRunning) {
374 logger.logAnimationCancelled(isPulse = false)
375 cancel()
376 }
377 }
378 }
379
380 /**
381 * Do we need a falsing check currently?
382 */
383 internal val isFalsingCheckNeeded: Boolean
384 get() = statusBarStateController.state == StatusBarState.KEYGUARD
385
386 /**
387 * Is dragging down enabled on a given view
388 * @param view The view to check or `null` to check if it's enabled at all
389 */
390 internal fun isDragDownEnabledForView(view: ExpandableView?): Boolean {
391 if (isDragDownAnywhereEnabled) {
392 return true
393 }
394 if (nsslController.isInLockedDownShade()) {
395 if (view == null) {
396 // Dragging down is allowed in general
397 return true
398 }
399 if (view is ExpandableNotificationRow) {
400 // Only drag down on sensitive views, otherwise the ExpandHelper will take this
401 return view.entry.isSensitive
402 }
403 }
404 return false
405 }
406
407 /**
408 * @return if drag down is enabled anywhere, not just on selected views.
409 */
410 internal val isDragDownAnywhereEnabled: Boolean
411 get() = (statusBarStateController.getState() == StatusBarState.KEYGUARD &&
412 !keyguardBypassController.bypassEnabled &&
413 (qS.isFullyCollapsed || useSplitShade))
414
415 /**
416 * The amount in pixels that the user has dragged down.
417 */
418 internal var dragDownAmount = 0f
419 set(value) {
420 if (field != value || forceApplyAmount) {
421 field = value
422 if (!nsslController.isInLockedDownShade() || field == 0f || forceApplyAmount) {
423 fractionToShade =
424 MathUtils.saturate(dragDownAmount / notificationShelfTransitionDistance)
425 nsslController.setTransitionToFullShadeAmount(fractionToShade)
426
427 qsTransitionController.dragDownAmount = value
428
429 callbacks.forEach { it.setTransitionToFullShadeAmount(field,
430 false /* animate */, 0 /* delay */) }
431
432 mediaHierarchyManager.setTransitionToFullShadeAmount(field)
433 scrimTransitionController.dragDownAmount = value
434 transitionToShadeAmountCommon(field)
435 keyguardTransitionController.dragDownAmount = value
436 shadeOverScroller.expansionDragDownAmount = dragDownAmount
437 }
438 }
439 }
440
441 private fun transitionToShadeAmountCommon(dragDownAmount: Float) {
442 if (depthControllerTransitionDistance == 0) { // split shade
443 depthController.transitionToFullShadeProgress = 0f
444 } else {
445 val depthProgress =
446 MathUtils.saturate(dragDownAmount / depthControllerTransitionDistance)
447 depthController.transitionToFullShadeProgress = depthProgress
448 }
449
450 val udfpsProgress = MathUtils.saturate(dragDownAmount / udfpsTransitionDistance)
451 udfpsKeyguardViewController?.setTransitionToFullShadeProgress(udfpsProgress)
452
453 val statusBarProgress = MathUtils.saturate(dragDownAmount / statusBarTransitionDistance)
454 centralSurfaces.setTransitionToFullShadeProgress(statusBarProgress)
455 }
456
457 private fun setDragDownAmountAnimated(
458 target: Float,
459 delay: Long = 0,
460 endlistener: (() -> Unit)? = null
461 ) {
462 logger.logDragDownAnimation(target)
463 val dragDownAnimator = ValueAnimator.ofFloat(dragDownAmount, target)
464 dragDownAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN
465 dragDownAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS
466 dragDownAnimator.addUpdateListener { animation: ValueAnimator ->
467 dragDownAmount = animation.animatedValue as Float
468 }
469 if (delay > 0) {
470 dragDownAnimator.startDelay = delay
471 }
472 if (endlistener != null) {
473 dragDownAnimator.addListener(object : AnimatorListenerAdapter() {
474 override fun onAnimationEnd(animation: Animator?) {
475 endlistener.invoke()
476 }
477 })
478 }
479 dragDownAnimator.start()
480 this.dragDownAnimator = dragDownAnimator
481 }
482
483 /**
484 * Animate appear the drag down amount.
485 */
486 private fun animateAppear(delay: Long = 0) {
487 // changing to shade locked will make isInLockDownShade true, so let's override
488 // that
489 forceApplyAmount = true
490
491 // we set the value initially to 1 pixel, since that will make sure we're
492 // transitioning to the full shade. this is important to avoid flickering,
493 // as the below animation only starts once the shade is unlocked, which can
494 // be a couple of frames later. if we're setting it to 0, it will use the
495 // default inset and therefore flicker
496 dragDownAmount = 1f
497 setDragDownAmountAnimated(fullTransitionDistanceByTap.toFloat(), delay = delay) {
498 // End listener:
499 // Reset
500 logger.logDragDownAmountReset()
501 dragDownAmount = 0f
502 forceApplyAmount = false
503 }
504 }
505
506 /**
507 * Ask this controller to go to the locked shade, changing the state change and doing
508 * an animation, where the qs appears from 0 from the top
509 *
510 * If secure with redaction: Show bouncer, go to unlocked shade.
511 * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
512 *
513 * Split shade is special case and [needsQSAnimation] will be always overridden to true.
514 * That's because handheld shade will automatically follow notifications animation, but that's
515 * not the case for split shade.
516 *
517 * @param expandView The view to expand after going to the shade
518 * @param needsQSAnimation if this needs the quick settings to slide in from the top or if
519 * that's already handled separately. This argument will be ignored on
520 * split shade as there QS animation can't be handled separately.
521 */
522 @JvmOverloads
523 fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) {
524 val isKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
525 logger.logTryGoToLockedShade(isKeyguard)
526 if (isKeyguard) {
527 val animationHandler: ((Long) -> Unit)?
528 if (needsQSAnimation || useSplitShade) {
529 // Let's use the default animation
530 animationHandler = null
531 } else {
532 // Let's only animate notifications
533 animationHandler = { delay: Long ->
534 notificationPanelController.animateToFullShade(delay)
535 }
536 }
537 goToLockedShadeInternal(expandedView, animationHandler,
538 cancelAction = null)
539 }
540 }
541
542 /**
543 * If secure with redaction: Show bouncer, go to unlocked shade.
544 *
545 * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
546 *
547 * @param expandView The view to expand after going to the shade.
548 * @param animationHandler The handler which performs the go to full shade animation. If null,
549 * the default handler will do the animation, otherwise the caller is
550 * responsible for the animation. The input value is a Long for the
551 * delay for the animation.
552 * @param cancelAction The runnable to invoke when the transition is aborted. This happens if
553 * the user goes to the bouncer and goes back.
554 */
555 private fun goToLockedShadeInternal(
556 expandView: View?,
557 animationHandler: ((Long) -> Unit)? = null,
558 cancelAction: Runnable? = null
559 ) {
560 if (centralSurfaces.isShadeDisabled) {
561 cancelAction?.run()
562 logger.logShadeDisabledOnGoToLockedShade()
563 return
564 }
565 var userId: Int = lockScreenUserManager.getCurrentUserId()
566 var entry: NotificationEntry? = null
567 if (expandView is ExpandableNotificationRow) {
568 entry = expandView.entry
569 entry.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */)
570 // Indicate that the group expansion is changing at this time -- this way the group
571 // and children backgrounds / divider animations will look correct.
572 entry.setGroupExpansionChanging(true)
573 userId = entry.sbn.userId
574 }
575 var fullShadeNeedsBouncer = (
576 !lockScreenUserManager.shouldShowLockscreenNotifications() ||
577 falsingCollector.shouldEnforceBouncer())
578 if (keyguardBypassController.bypassEnabled) {
579 fullShadeNeedsBouncer = false
580 }
581 if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
582 statusBarStateController.setLeaveOpenOnKeyguardHide(true)
583 var onDismissAction: OnDismissAction? = null
584 if (animationHandler != null) {
585 onDismissAction = OnDismissAction {
586 // We're waiting on keyguard to hide before triggering the action,
587 // as that will make the animation work properly
588 animationHandlerOnKeyguardDismiss = animationHandler
589 false
590 }
591 }
592 val cancelHandler = Runnable {
593 draggedDownEntry?.apply {
594 setUserLocked(false)
595 notifyHeightChanged(false /* needsAnimation */)
596 draggedDownEntry = null
597 }
598 cancelAction?.run()
599 }
600 logger.logShowBouncerOnGoToLockedShade()
601 centralSurfaces.showBouncerWithDimissAndCancelIfKeyguard(onDismissAction, cancelHandler)
602 draggedDownEntry = entry
603 } else {
604 logger.logGoingToLockedShade(animationHandler != null)
605 if (statusBarStateController.isDozing) {
606 // Make sure we don't go back to keyguard immediately again after waking up
607 isWakingToShadeLocked = true
608 }
609 statusBarStateController.setState(StatusBarState.SHADE_LOCKED)
610 // This call needs to be after updating the shade state since otherwise
611 // the scrimstate resets too early
612 if (animationHandler != null) {
613 animationHandler.invoke(0 /* delay */)
614 } else {
615 performDefaultGoToFullShadeAnimation(0)
616 }
617 }
618 }
619
620 /**
621 * Notify this handler that the keyguard was just dismissed and that a animation to
622 * the full shade should happen.
623 *
624 * @param delay the delay to do the animation with
625 * @param previousState which state were we in when we hid the keyguard?
626 */
627 fun onHideKeyguard(delay: Long, previousState: Int) {
628 logger.logOnHideKeyguard()
629 if (animationHandlerOnKeyguardDismiss != null) {
630 animationHandlerOnKeyguardDismiss!!.invoke(delay)
631 animationHandlerOnKeyguardDismiss = null
632 } else {
633 if (nextHideKeyguardNeedsNoAnimation) {
634 nextHideKeyguardNeedsNoAnimation = false
635 } else if (previousState != StatusBarState.SHADE_LOCKED) {
636 // No animation necessary if we already were in the shade locked!
637 performDefaultGoToFullShadeAnimation(delay)
638 }
639 }
640 draggedDownEntry?.apply {
641 setUserLocked(false)
642 draggedDownEntry = null
643 }
644 }
645
646 /**
647 * Perform the default appear animation when going to the full shade. This is called when
648 * not triggered by gestures, e.g. when clicking on the shelf or expand button.
649 */
650 private fun performDefaultGoToFullShadeAnimation(delay: Long) {
651 logger.logDefaultGoToFullShadeAnimation(delay)
652 notificationPanelController.animateToFullShade(delay)
653 animateAppear(delay)
654 }
655
656 //
657 // PULSE EXPANSION
658 //
659
660 /**
661 * Set the height how tall notifications are pulsing. This is only set whenever we are expanding
662 * from a pulse and determines how much the notifications are expanded.
663 */
664 fun setPulseHeight(height: Float, animate: Boolean = false) {
665 if (animate) {
666 val pulseHeightAnimator = ValueAnimator.ofFloat(pulseHeight, height)
667 pulseHeightAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN
668 pulseHeightAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS
669 pulseHeightAnimator.addUpdateListener { animation: ValueAnimator ->
670 setPulseHeight(animation.animatedValue as Float)
671 }
672 pulseHeightAnimator.start()
673 this.pulseHeightAnimator = pulseHeightAnimator
674 } else {
675 pulseHeight = height
676 val overflow = nsslController.setPulseHeight(height)
677 notificationPanelController.setOverStretchAmount(overflow)
678 val transitionHeight = if (keyguardBypassController.bypassEnabled) height else 0.0f
679 transitionToShadeAmountCommon(transitionHeight)
680 }
681 }
682
683 /**
684 * Finish the pulse animation when the touch interaction finishes
685 * @param cancelled was the interaction cancelled and this is a reset?
686 */
687 fun finishPulseAnimation(cancelled: Boolean) {
688 logger.logPulseExpansionFinished(cancelled)
689 if (cancelled) {
690 setPulseHeight(0f, animate = true)
691 } else {
692 callbacks.forEach { it.onPulseExpansionFinished() }
693 setPulseHeight(0f, animate = false)
694 }
695 }
696
697 /**
698 * Notify this class that a pulse expansion is starting
699 */
700 fun onPulseExpansionStarted() {
701 logger.logPulseExpansionStarted()
702 pulseHeightAnimator?.apply {
703 if (isRunning) {
704 logger.logAnimationCancelled(isPulse = true)
705 cancel()
706 }
707 }
708 }
709
710 override fun dump(pw: PrintWriter, args: Array<out String>) {
711 IndentingPrintWriter(pw, " ").let {
712 it.println("LSShadeTransitionController:")
713 it.increaseIndent()
714 it.println("pulseHeight: $pulseHeight")
715 it.println("useSplitShade: $useSplitShade")
716 it.println("dragDownAmount: $dragDownAmount")
717 it.println("isDragDownAnywhereEnabled: $isDragDownAnywhereEnabled")
718 it.println("isFalsingCheckNeeded: $isFalsingCheckNeeded")
719 it.println("isWakingToShadeLocked: $isWakingToShadeLocked")
720 it.println("hasPendingHandlerOnKeyguardDismiss: " +
721 "${animationHandlerOnKeyguardDismiss != null}")
722 }
723 }
724
725
726 fun addCallback(callback: Callback) {
727 if (!callbacks.contains(callback)) {
728 callbacks.add(callback)
729 }
730 }
731
732 /**
733 * Callback for authentication events.
734 */
735 interface Callback {
736 /** TODO: comment here */
737 fun onPulseExpansionFinished() {}
738
739 /**
740 * Sets the amount of pixels we have currently dragged down if we're transitioning
741 * to the full shade. 0.0f means we're not transitioning yet.
742 */
743 fun setTransitionToFullShadeAmount(pxAmount: Float, animate: Boolean, delay: Long) {}
744 }
745 }
746
747 /**
748 * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
749 * the notification where the drag started.
750 */
751 class DragDownHelper(
752 private val falsingManager: FalsingManager,
753 private val falsingCollector: FalsingCollector,
754 private val dragDownCallback: LockscreenShadeTransitionController,
755 context: Context
756 ) : Gefingerpoken {
757
758 private var dragDownAmountOnStart = 0.0f
759 lateinit var expandCallback: ExpandHelper.Callback
760
761 private var minDragDistance = 0
762 private var initialTouchX = 0f
763 private var initialTouchY = 0f
764 private var touchSlop = 0f
765 private var slopMultiplier = 0f
766 private var draggedFarEnough = false
767 private var startingChild: ExpandableView? = null
768 private var lastHeight = 0f
769 var isDraggingDown = false
770 private set
771
772 private val isFalseTouch: Boolean
773 get() {
774 return if (!dragDownCallback.isFalsingCheckNeeded) {
775 false
776 } else {
777 falsingManager.isFalseTouch(Classifier.NOTIFICATION_DRAG_DOWN) || !draggedFarEnough
778 }
779 }
780
781 val isDragDownEnabled: Boolean
782 get() = dragDownCallback.isDragDownEnabledForView(null)
783
<lambda>null784 init {
785 updateResources(context)
786 }
787
updateResourcesnull788 fun updateResources(context: Context) {
789 minDragDistance = context.resources.getDimensionPixelSize(
790 R.dimen.keyguard_drag_down_min_distance)
791 val configuration = ViewConfiguration.get(context)
792 touchSlop = configuration.scaledTouchSlop.toFloat()
793 slopMultiplier = configuration.scaledAmbiguousGestureMultiplier
794 }
795
onInterceptTouchEventnull796 override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
797 val x = event.x
798 val y = event.y
799 when (event.actionMasked) {
800 MotionEvent.ACTION_DOWN -> {
801 draggedFarEnough = false
802 isDraggingDown = false
803 startingChild = null
804 initialTouchY = y
805 initialTouchX = x
806 }
807 MotionEvent.ACTION_MOVE -> {
808 val h = y - initialTouchY
809 // Adjust the touch slop if another gesture may be being performed.
810 val touchSlop = if (event.classification
811 == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE)
812 touchSlop * slopMultiplier
813 else
814 touchSlop
815 if (h > touchSlop && h > Math.abs(x - initialTouchX)) {
816 falsingCollector.onNotificationStartDraggingDown()
817 isDraggingDown = true
818 captureStartingChild(initialTouchX, initialTouchY)
819 initialTouchY = y
820 initialTouchX = x
821 dragDownCallback.onDragDownStarted(startingChild)
822 dragDownAmountOnStart = dragDownCallback.dragDownAmount
823 return startingChild != null || dragDownCallback.isDragDownAnywhereEnabled
824 }
825 }
826 }
827 return false
828 }
829
onTouchEventnull830 override fun onTouchEvent(event: MotionEvent): Boolean {
831 if (!isDraggingDown) {
832 return false
833 }
834 val x = event.x
835 val y = event.y
836 when (event.actionMasked) {
837 MotionEvent.ACTION_MOVE -> {
838 lastHeight = y - initialTouchY
839 captureStartingChild(initialTouchX, initialTouchY)
840 dragDownCallback.dragDownAmount = lastHeight + dragDownAmountOnStart
841 if (startingChild != null) {
842 handleExpansion(lastHeight, startingChild!!)
843 }
844 if (lastHeight > minDragDistance) {
845 if (!draggedFarEnough) {
846 draggedFarEnough = true
847 dragDownCallback.onCrossedThreshold(true)
848 }
849 } else {
850 if (draggedFarEnough) {
851 draggedFarEnough = false
852 dragDownCallback.onCrossedThreshold(false)
853 }
854 }
855 return true
856 }
857 MotionEvent.ACTION_UP -> if (!falsingManager.isUnlockingDisabled && !isFalseTouch &&
858 dragDownCallback.canDragDown()) {
859 dragDownCallback.onDraggedDown(startingChild, (y - initialTouchY).toInt())
860 if (startingChild != null) {
861 expandCallback.setUserLockedChild(startingChild, false)
862 startingChild = null
863 }
864 isDraggingDown = false
865 } else {
866 stopDragging()
867 return false
868 }
869 MotionEvent.ACTION_CANCEL -> {
870 stopDragging()
871 return false
872 }
873 }
874 return false
875 }
876
captureStartingChildnull877 private fun captureStartingChild(x: Float, y: Float) {
878 if (startingChild == null) {
879 startingChild = findView(x, y)
880 if (startingChild != null) {
881 if (dragDownCallback.isDragDownEnabledForView(startingChild)) {
882 expandCallback.setUserLockedChild(startingChild, true)
883 } else {
884 startingChild = null
885 }
886 }
887 }
888 }
889
handleExpansionnull890 private fun handleExpansion(heightDelta: Float, child: ExpandableView) {
891 var hDelta = heightDelta
892 if (hDelta < 0) {
893 hDelta = 0f
894 }
895 val expandable = child.isContentExpandable
896 val rubberbandFactor = if (expandable) {
897 RUBBERBAND_FACTOR_EXPANDABLE
898 } else {
899 RUBBERBAND_FACTOR_STATIC
900 }
901 var rubberband = hDelta * rubberbandFactor
902 if (expandable && rubberband + child.collapsedHeight > child.maxContentHeight) {
903 var overshoot = rubberband + child.collapsedHeight - child.maxContentHeight
904 overshoot *= 1 - RUBBERBAND_FACTOR_STATIC
905 rubberband -= overshoot
906 }
907 child.actualHeight = (child.collapsedHeight + rubberband).toInt()
908 }
909
910 @VisibleForTesting
cancelChildExpansionnull911 fun cancelChildExpansion(
912 child: ExpandableView,
913 animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS
914 ) {
915 if (child.actualHeight == child.collapsedHeight) {
916 expandCallback.setUserLockedChild(child, false)
917 return
918 }
919 val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight)
920 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
921 anim.duration = animationDuration
922 anim.addUpdateListener { animation: ValueAnimator ->
923 // don't use reflection, because the `actualHeight` field may be obfuscated
924 child.actualHeight = animation.animatedValue as Int
925 }
926 anim.addListener(object : AnimatorListenerAdapter() {
927 override fun onAnimationEnd(animation: Animator) {
928 expandCallback.setUserLockedChild(child, false)
929 }
930 })
931 anim.start()
932 }
933
stopDraggingnull934 private fun stopDragging() {
935 falsingCollector.onNotificationStopDraggingDown()
936 if (startingChild != null) {
937 cancelChildExpansion(startingChild!!)
938 startingChild = null
939 }
940 isDraggingDown = false
941 dragDownCallback.onDragDownReset()
942 }
943
findViewnull944 private fun findView(x: Float, y: Float): ExpandableView? {
945 return expandCallback.getChildAtRawPosition(x, y)
946 }
947 }
948