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