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