1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.keyguard.ui.binder
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.annotation.DrawableRes
22 import android.annotation.SuppressLint
23 import android.graphics.Point
24 import android.graphics.Rect
25 import android.util.Log
26 import android.view.HapticFeedbackConstants
27 import android.view.View
28 import android.view.View.OnLayoutChangeListener
29 import android.view.View.VISIBLE
30 import android.view.ViewGroup
31 import android.view.ViewGroup.OnHierarchyChangeListener
32 import android.view.ViewPropertyAnimator
33 import android.view.WindowInsets
34 import androidx.activity.OnBackPressedDispatcher
35 import androidx.activity.OnBackPressedDispatcherOwner
36 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
37 import androidx.lifecycle.Lifecycle
38 import androidx.lifecycle.repeatOnLifecycle
39 import com.android.app.animation.Interpolators
40 import com.android.internal.jank.InteractionJankMonitor
41 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
42 import com.android.systemui.Flags.newAodTransition
43 import com.android.systemui.common.shared.model.Icon
44 import com.android.systemui.common.shared.model.Text
45 import com.android.systemui.common.shared.model.TintedIcon
46 import com.android.systemui.common.ui.ConfigurationState
47 import com.android.systemui.common.ui.view.onApplyWindowInsets
48 import com.android.systemui.common.ui.view.onLayoutChanged
49 import com.android.systemui.common.ui.view.onTouchListener
50 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
51 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
52 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
53 import com.android.systemui.keyguard.KeyguardViewMediator
54 import com.android.systemui.keyguard.MigrateClocksToBlueprint
55 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
56 import com.android.systemui.keyguard.shared.ComposeLockscreen
57 import com.android.systemui.keyguard.shared.model.KeyguardState
58 import com.android.systemui.keyguard.shared.model.TransitionState
59 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
60 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
61 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
62 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
63 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
64 import com.android.systemui.keyguard.ui.viewmodel.TransitionData
65 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
66 import com.android.systemui.lifecycle.repeatWhenAttached
67 import com.android.systemui.plugins.FalsingManager
68 import com.android.systemui.res.R
69 import com.android.systemui.shade.domain.interactor.ShadeInteractor
70 import com.android.systemui.statusbar.CrossFadeHelper
71 import com.android.systemui.statusbar.VibratorHelper
72 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
73 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
74 import com.android.systemui.temporarydisplay.ViewPriority
75 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
76 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
77 import com.android.systemui.util.kotlin.DisposableHandles
78 import com.android.systemui.util.ui.AnimatedValue
79 import com.android.systemui.util.ui.isAnimating
80 import com.android.systemui.util.ui.stopAnimating
81 import com.android.systemui.util.ui.value
82 import kotlin.math.min
83 import kotlinx.coroutines.DisposableHandle
84 import kotlinx.coroutines.ExperimentalCoroutinesApi
85 import kotlinx.coroutines.coroutineScope
86 import kotlinx.coroutines.flow.Flow
87 import kotlinx.coroutines.flow.MutableStateFlow
88 import kotlinx.coroutines.flow.stateIn
89 import kotlinx.coroutines.flow.update
90 import kotlinx.coroutines.launch
91 
92 /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */
93 @OptIn(ExperimentalCoroutinesApi::class)
94 object KeyguardRootViewBinder {
95     @SuppressLint("ClickableViewAccessibility")
96     @JvmStatic
97     fun bind(
98         view: ViewGroup,
99         viewModel: KeyguardRootViewModel,
100         blueprintViewModel: KeyguardBlueprintViewModel,
101         configuration: ConfigurationState,
102         occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?,
103         chipbarCoordinator: ChipbarCoordinator?,
104         screenOffAnimationController: ScreenOffAnimationController,
105         shadeInteractor: ShadeInteractor,
106         clockInteractor: KeyguardClockInteractor,
107         clockViewModel: KeyguardClockViewModel,
108         interactionJankMonitor: InteractionJankMonitor?,
109         deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
110         vibratorHelper: VibratorHelper?,
111         falsingManager: FalsingManager?,
112         keyguardViewMediator: KeyguardViewMediator?,
113     ): DisposableHandle {
114         val disposables = DisposableHandles()
115         val childViews = mutableMapOf<Int, View>()
116 
117         if (KeyguardBottomAreaRefactor.isEnabled) {
118             disposables +=
119                 view.onTouchListener { _, event ->
120                     if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
121                         viewModel.setRootViewLastTapPosition(
122                             Point(event.x.toInt(), event.y.toInt())
123                         )
124                     }
125                     false
126                 }
127         }
128 
129         val burnInParams = MutableStateFlow(BurnInParameters())
130         val viewState = ViewStateAccessor(alpha = { view.alpha })
131         disposables +=
132             view.repeatWhenAttached {
133                 repeatOnLifecycle(Lifecycle.State.CREATED) {
134                     if (ComposeLockscreen.isEnabled) {
135                         view.setViewTreeOnBackPressedDispatcherOwner(
136                             object : OnBackPressedDispatcherOwner {
137                                 override val onBackPressedDispatcher =
138                                     OnBackPressedDispatcher().apply {
139                                         setOnBackInvokedDispatcher(
140                                             view.viewRootImpl.onBackInvokedDispatcher
141                                         )
142                                     }
143 
144                                 override val lifecycle: Lifecycle =
145                                     this@repeatWhenAttached.lifecycle
146                             }
147                         )
148                     }
149                     launch {
150                         occludingAppDeviceEntryMessageViewModel?.message?.collect { biometricMessage
151                             ->
152                             if (biometricMessage?.message != null) {
153                                 chipbarCoordinator!!.displayView(
154                                     createChipbarInfo(
155                                         biometricMessage.message,
156                                         R.drawable.ic_lock,
157                                     )
158                                 )
159                             } else {
160                                 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
161                             }
162                         }
163                     }
164 
165                     if (
166                         KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled
167                     ) {
168                         launch {
169                             viewModel.alpha(viewState).collect { alpha ->
170                                 view.alpha = alpha
171                                 if (KeyguardBottomAreaRefactor.isEnabled) {
172                                     childViews[statusViewId]?.alpha = alpha
173                                     childViews[burnInLayerId]?.alpha = alpha
174                                 }
175                             }
176                         }
177                     }
178 
179                     if (MigrateClocksToBlueprint.isEnabled) {
180                         launch {
181                             viewModel.burnInLayerVisibility.collect { visibility ->
182                                 childViews[burnInLayerId]?.visibility = visibility
183                                 childViews[aodNotificationIconContainerId]?.visibility = visibility
184                             }
185                         }
186 
187                         launch {
188                             viewModel.burnInLayerAlpha.collect { alpha ->
189                                 childViews[statusViewId]?.alpha = alpha
190                                 childViews[aodNotificationIconContainerId]?.alpha = alpha
191                             }
192                         }
193 
194                         launch {
195                             val clipBounds = Rect()
196                             viewModel.topClippingBounds.collect { clipTop ->
197                                 if (clipTop == null) {
198                                     view.setClipBounds(null)
199                                 } else {
200                                     clipBounds.apply {
201                                         top = clipTop
202                                         left = view.getLeft()
203                                         right = view.getRight()
204                                         bottom = view.getBottom()
205                                     }
206                                     view.setClipBounds(clipBounds)
207                                 }
208                             }
209                         }
210 
211                         launch {
212                             viewModel.lockscreenStateAlpha(viewState).collect { alpha ->
213                                 childViews[statusViewId]?.alpha = alpha
214                             }
215                         }
216 
217                         launch {
218                             // When translation happens in burnInLayer, it won't be weather clock
219                             // large clock isn't added to burnInLayer due to its scale transition
220                             // so we also need to add translation to it here
221                             // same as translationX
222                             viewModel.translationY.collect { y ->
223                                 childViews[burnInLayerId]?.translationY = y
224                                 childViews[largeClockId]?.translationY = y
225                                 childViews[aodNotificationIconContainerId]?.translationY = y
226                             }
227                         }
228 
229                         launch {
230                             viewModel.translationX.collect { state ->
231                                 val px = state.value ?: return@collect
232                                 when {
233                                     state.isToOrFrom(KeyguardState.AOD) -> {
234                                         // Large Clock is not translated in the x direction
235                                         childViews[burnInLayerId]?.translationX = px
236                                         childViews[aodNotificationIconContainerId]?.translationX =
237                                             px
238                                     }
239                                     state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> {
240                                         for ((key, childView) in childViews.entries) {
241                                             when (key) {
242                                                 indicationArea,
243                                                 startButton,
244                                                 endButton,
245                                                 lockIcon,
246                                                 deviceEntryIcon -> {
247                                                     // Do not move these views
248                                                 }
249                                                 else -> childView.translationX = px
250                                             }
251                                         }
252                                     }
253                                 }
254                             }
255                         }
256 
257                         launch {
258                             viewModel.scale.collect { scaleViewModel ->
259                                 if (scaleViewModel.scaleClockOnly) {
260                                     // For clocks except weather clock, we have scale transition
261                                     // besides translate
262                                     childViews[largeClockId]?.let {
263                                         it.scaleX = scaleViewModel.scale
264                                         it.scaleY = scaleViewModel.scale
265                                     }
266                                 }
267                             }
268                         }
269 
270                         if (NotificationIconContainerRefactor.isEnabled) {
271                             launch {
272                                 val iconsAppearTranslationPx =
273                                     configuration
274                                         .getDimensionPixelSize(R.dimen.shelf_appear_translation)
275                                         .stateIn(this)
276                                 viewModel.isNotifIconContainerVisible.collect { isVisible ->
277                                     childViews[aodNotificationIconContainerId]
278                                         ?.setAodNotifIconContainerIsVisible(
279                                             isVisible,
280                                             iconsAppearTranslationPx.value,
281                                             screenOffAnimationController,
282                                         )
283                                 }
284                             }
285                         }
286 
287                         interactionJankMonitor?.let { jankMonitor ->
288                             launch {
289                                 viewModel.goneToAodTransition.collect {
290                                     when (it.transitionState) {
291                                         TransitionState.STARTED -> {
292                                             val clockId = clockInteractor.renderedClockId
293                                             val builder =
294                                                 InteractionJankMonitor.Configuration.Builder
295                                                     .withView(CUJ_SCREEN_OFF_SHOW_AOD, view)
296                                                     .setTag(clockId)
297                                             jankMonitor.begin(builder)
298                                         }
299                                         TransitionState.CANCELED ->
300                                             jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
301                                         TransitionState.FINISHED -> {
302                                             if (MigrateClocksToBlueprint.isEnabled) {
303                                                 keyguardViewMediator?.maybeHandlePendingLock()
304                                             }
305                                             jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
306                                         }
307                                         TransitionState.RUNNING -> Unit
308                                     }
309                                 }
310                             }
311                         }
312                     }
313 
314                     launch {
315                         shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded ->
316                             view.visibility =
317                                 if (isFullyAnyExpanded) {
318                                     View.INVISIBLE
319                                 } else {
320                                     View.VISIBLE
321                                 }
322                         }
323                     }
324 
325                     launch { burnInParams.collect { viewModel.updateBurnInParams(it) } }
326 
327                     if (deviceEntryHapticsInteractor != null && vibratorHelper != null) {
328                         launch {
329                             deviceEntryHapticsInteractor.playSuccessHaptic.collect {
330                                 vibratorHelper.performHapticFeedback(
331                                     view,
332                                     HapticFeedbackConstants.CONFIRM,
333                                     HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
334                                 )
335                             }
336                         }
337 
338                         launch {
339                             deviceEntryHapticsInteractor.playErrorHaptic.collect {
340                                 vibratorHelper.performHapticFeedback(
341                                     view,
342                                     HapticFeedbackConstants.REJECT,
343                                     HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
344                                 )
345                             }
346                         }
347                     }
348                 }
349             }
350 
351         if (MigrateClocksToBlueprint.isEnabled) {
352             burnInParams.update { current ->
353                 current.copy(translationY = { childViews[burnInLayerId]?.translationY })
354             }
355         }
356 
357         disposables +=
358             view.onLayoutChanged(
359                 OnLayoutChange(
360                     viewModel,
361                     blueprintViewModel,
362                     clockViewModel,
363                     childViews,
364                     burnInParams
365                 )
366             )
367 
368         // Views will be added or removed after the call to bind(). This is needed to avoid many
369         // calls to findViewById
370         view.setOnHierarchyChangeListener(
371             object : OnHierarchyChangeListener {
372                 override fun onChildViewAdded(parent: View, child: View) {
373                     childViews.put(child.id, child)
374                 }
375 
376                 override fun onChildViewRemoved(parent: View, child: View) {
377                     childViews.remove(child.id)
378                 }
379             }
380         )
381         disposables += DisposableHandle {
382             view.setOnHierarchyChangeListener(null)
383             childViews.clear()
384         }
385 
386         disposables +=
387             view.onApplyWindowInsets { _: View, insets: WindowInsets ->
388                 val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
389                 burnInParams.update { current ->
390                     current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
391                 }
392                 insets
393             }
394 
395         return disposables
396     }
397 
398     /**
399      * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
400      */
401     private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
402         return ChipbarInfo(
403             startIcon =
404                 TintedIcon(
405                     Icon.Resource(icon, null),
406                     ChipbarInfo.DEFAULT_ICON_TINT,
407                 ),
408             text = Text.Loaded(message),
409             endItem = null,
410             vibrationEffect = null,
411             windowTitle = "OccludingAppUnlockMsgChip",
412             wakeReason = "OCCLUDING_APP_UNLOCK_MSG_CHIP",
413             timeoutMs = 3500,
414             id = ID,
415             priority = ViewPriority.CRITICAL,
416             instanceId = null,
417         )
418     }
419 
420     private class OnLayoutChange(
421         private val viewModel: KeyguardRootViewModel,
422         private val blueprintViewModel: KeyguardBlueprintViewModel,
423         private val clockViewModel: KeyguardClockViewModel,
424         private val childViews: Map<Int, View>,
425         private val burnInParams: MutableStateFlow<BurnInParameters>,
426     ) : OnLayoutChangeListener {
427         var prevTransition: TransitionData? = null
428 
429         override fun onLayoutChange(
430             view: View,
431             left: Int,
432             top: Int,
433             right: Int,
434             bottom: Int,
435             oldLeft: Int,
436             oldTop: Int,
437             oldRight: Int,
438             oldBottom: Int
439         ) {
440             // After layout, ensure the notifications are positioned correctly
441             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
442                 // Do not update a second time while a blueprint transition is running
443                 val transition = blueprintViewModel.currentTransition.value
444                 val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
445                 if (prevTransition == transition && shouldAnimate) {
446                     if (DEBUG) Log.w(TAG, "Skipping; layout during transition")
447                     return
448                 }
449 
450                 prevTransition = transition
451                 viewModel.onNotificationContainerBoundsChanged(
452                     notificationListPlaceholder.top.toFloat(),
453                     notificationListPlaceholder.bottom.toFloat(),
454                     animate = shouldAnimate
455                 )
456             }
457 
458             burnInParams.update { current ->
459                 current.copy(
460                     minViewY =
461                         if (MigrateClocksToBlueprint.isEnabled) {
462                             // To ensure burn-in doesn't enroach the top inset, get the min top Y
463                             childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
464                                 min(
465                                     currentMin,
466                                     if (!isUserVisible(view)) {
467                                         Int.MAX_VALUE
468                                     } else {
469                                         view.getTop()
470                                     }
471                                 )
472                             }
473                         } else {
474                             childViews[statusViewId]?.top ?: 0
475                         }
476                 )
477             }
478         }
479 
480         private fun isUserVisible(view: View): Boolean {
481             return view.id != burnInLayerId &&
482                 view.visibility == VISIBLE &&
483                 view.width > 0 &&
484                 view.height > 0
485         }
486     }
487 
488     suspend fun bindAodNotifIconVisibility(
489         view: View,
490         isVisible: Flow<AnimatedValue<Boolean>>,
491         configuration: ConfigurationState,
492         screenOffAnimationController: ScreenOffAnimationController,
493     ) {
494         if (MigrateClocksToBlueprint.isEnabled) {
495             throw IllegalStateException("should only be called in legacy code paths")
496         }
497         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
498         coroutineScope {
499             val iconAppearTranslationPx =
500                 configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
501             isVisible.collect { isVisible ->
502                 view.setAodNotifIconContainerIsVisible(
503                     isVisible = isVisible,
504                     iconsAppearTranslationPx = iconAppearTranslationPx.value,
505                     screenOffAnimationController = screenOffAnimationController,
506                 )
507             }
508         }
509     }
510 
511     private fun View.setAodNotifIconContainerIsVisible(
512         isVisible: AnimatedValue<Boolean>,
513         iconsAppearTranslationPx: Int,
514         screenOffAnimationController: ScreenOffAnimationController,
515     ) {
516         animate().cancel()
517         val animatorListener =
518             object : AnimatorListenerAdapter() {
519                 override fun onAnimationEnd(animation: Animator) {
520                     isVisible.stopAnimating()
521                 }
522             }
523         when {
524             !isVisible.isAnimating -> {
525                 if (!MigrateClocksToBlueprint.isEnabled) {
526                     translationY = 0f
527                 }
528                 visibility =
529                     if (isVisible.value) {
530                         alpha = 1f
531                         View.VISIBLE
532                     } else {
533                         alpha = 0f
534                         View.INVISIBLE
535                     }
536             }
537             newAodTransition() -> {
538                 animateInIconTranslation()
539                 if (isVisible.value) {
540                     CrossFadeHelper.fadeIn(this, animatorListener)
541                 } else {
542                     CrossFadeHelper.fadeOut(this, animatorListener)
543                 }
544             }
545             !isVisible.value -> {
546                 // Let's make sure the icon are translated to 0, since we cancelled it above
547                 animateInIconTranslation()
548                 CrossFadeHelper.fadeOut(this, animatorListener)
549             }
550             visibility != View.VISIBLE -> {
551                 // No fading here, let's just appear the icons instead!
552                 visibility = View.VISIBLE
553                 alpha = 1f
554                 appearIcons(
555                     animate = screenOffAnimationController.shouldAnimateAodIcons(),
556                     iconsAppearTranslationPx,
557                     animatorListener,
558                 )
559             }
560             else -> {
561                 // Let's make sure the icons are translated to 0, since we cancelled it above
562                 animateInIconTranslation()
563                 // We were fading out, let's fade in instead
564                 CrossFadeHelper.fadeIn(this, animatorListener)
565             }
566         }
567     }
568 
569     private fun View.appearIcons(
570         animate: Boolean,
571         iconAppearTranslation: Int,
572         animatorListener: Animator.AnimatorListener,
573     ) {
574         if (animate) {
575             if (!MigrateClocksToBlueprint.isEnabled) {
576                 translationY = -iconAppearTranslation.toFloat()
577             }
578             alpha = 0f
579             animate()
580                 .alpha(1f)
581                 .setInterpolator(Interpolators.LINEAR)
582                 .setDuration(AOD_ICONS_APPEAR_DURATION)
583                 .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() }
584                 .setListener(animatorListener)
585                 .start()
586         } else {
587             alpha = 1.0f
588             if (!MigrateClocksToBlueprint.isEnabled) {
589                 translationY = 0f
590             }
591         }
592     }
593 
594     private fun View.animateInIconTranslation() {
595         if (!MigrateClocksToBlueprint.isEnabled) {
596             animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
597         }
598     }
599 
600     private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator =
601         setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f)
602 
603     private val statusViewId = R.id.keyguard_status_view
604     private val burnInLayerId = R.id.burn_in_layer
605     private val aodNotificationIconContainerId = R.id.aod_notification_icon_container
606     private val largeClockId = R.id.lockscreen_clock_view_large
607     private val smallClockId = R.id.lockscreen_clock_view
608     private val indicationArea = R.id.keyguard_indication_area
609     private val startButton = R.id.start_button
610     private val endButton = R.id.end_button
611     private val lockIcon = R.id.lock_icon_view
612     private val deviceEntryIcon = R.id.device_entry_icon_view
613     private val nsslPlaceholderId = R.id.nssl_placeholder
614 
615     private const val ID = "occluding_app_device_entry_unlock_msg"
616     private const val AOD_ICONS_APPEAR_DURATION: Long = 200
617     private const val TAG = "KeyguardRootViewBinder"
618     private const val DEBUG = false
619 }
620