• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.view.HapticFeedbackConstants
26 import android.view.InputDevice
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.View.GONE
30 import android.view.View.INVISIBLE
31 import android.view.View.OnLayoutChangeListener
32 import android.view.View.VISIBLE
33 import android.view.ViewGroup
34 import android.view.ViewGroup.OnHierarchyChangeListener
35 import android.view.WindowInsets
36 import androidx.activity.OnBackPressedDispatcher
37 import androidx.activity.OnBackPressedDispatcherOwner
38 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
39 import androidx.lifecycle.Lifecycle
40 import androidx.lifecycle.repeatOnLifecycle
41 import com.android.app.tracing.coroutines.launchTraced as launch
42 import com.android.keyguard.AuthInteractionProperties
43 import com.android.systemui.Flags
44 import com.android.systemui.Flags.msdlFeedback
45 import com.android.systemui.common.shared.model.Icon
46 import com.android.systemui.common.shared.model.Text
47 import com.android.systemui.common.shared.model.TintedIcon
48 import com.android.systemui.common.ui.ConfigurationState
49 import com.android.systemui.common.ui.view.onApplyWindowInsets
50 import com.android.systemui.common.ui.view.onLayoutChanged
51 import com.android.systemui.common.ui.view.onTouchListener
52 import com.android.systemui.customization.R as customR
53 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
54 import com.android.systemui.keyguard.shared.model.KeyguardState
55 import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection
56 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
57 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
58 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
59 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
60 import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
61 import com.android.systemui.keyguard.ui.viewmodel.TransitionData
62 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
63 import com.android.systemui.lifecycle.repeatWhenAttached
64 import com.android.systemui.log.LogBuffer
65 import com.android.systemui.log.core.Logger
66 import com.android.systemui.log.dagger.KeyguardBlueprintLog
67 import com.android.systemui.plugins.FalsingManager
68 import com.android.systemui.res.R
69 import com.android.systemui.scene.shared.flag.SceneContainerFlag
70 import com.android.systemui.shade.domain.interactor.ShadeInteractor
71 import com.android.systemui.shared.R as sharedR
72 import com.android.systemui.statusbar.CrossFadeHelper
73 import com.android.systemui.statusbar.VibratorHelper
74 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
75 import com.android.systemui.temporarydisplay.ViewPriority
76 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
77 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
78 import com.android.systemui.util.kotlin.DisposableHandles
79 import com.android.systemui.util.ui.AnimatedValue
80 import com.android.systemui.util.ui.isAnimating
81 import com.android.systemui.util.ui.stopAnimating
82 import com.android.systemui.util.ui.value
83 import com.android.systemui.wallpapers.ui.viewmodel.WallpaperFocalAreaViewModel
84 import com.google.android.msdl.data.model.MSDLToken
85 import com.google.android.msdl.domain.MSDLPlayer
86 import kotlin.math.min
87 import kotlinx.coroutines.CoroutineDispatcher
88 import kotlinx.coroutines.DisposableHandle
89 import kotlinx.coroutines.flow.MutableStateFlow
90 import kotlinx.coroutines.flow.stateIn
91 import kotlinx.coroutines.flow.update
92 
93 /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */
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         shadeInteractor: ShadeInteractor,
105         smartspaceViewModel: KeyguardSmartspaceViewModel,
106         deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
107         vibratorHelper: VibratorHelper?,
108         falsingManager: FalsingManager?,
109         statusBarKeyguardViewManager: StatusBarKeyguardViewManager?,
110         mainImmediateDispatcher: CoroutineDispatcher,
111         msdlPlayer: MSDLPlayer?,
112         @KeyguardBlueprintLog blueprintLog: LogBuffer,
113         wallpaperFocalAreaViewModel: WallpaperFocalAreaViewModel,
114     ): DisposableHandle {
115         val disposables = DisposableHandles()
116         val childViews = mutableMapOf<Int, View>()
117 
118         disposables +=
119             view.onTouchListener { _, event ->
120                 var consumed = false
121                 if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
122                     // signifies a primary button click down has reached keyguardrootview
123                     // we need to return true here otherwise an ACTION_UP will never arrive
124                     if (Flags.nonTouchscreenDevicesBypassFalsing()) {
125                         if (
126                             event.action == MotionEvent.ACTION_DOWN &&
127                                 event.buttonState == MotionEvent.BUTTON_PRIMARY &&
128                                 !event.isTouchscreenSource()
129                         ) {
130                             consumed = true
131                         } else if (
132                             event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource()
133                         ) {
134                             statusBarKeyguardViewManager?.showBouncer(
135                                 true,
136                                 "KeyguardRootViewBinder: click on lockscreen",
137                             )
138                             consumed = true
139                         }
140                     }
141                     viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt()))
142                 }
143                 consumed
144             }
145 
146         val burnInParams = MutableStateFlow(BurnInParameters())
147         val viewState = ViewStateAccessor(alpha = { view.alpha })
148 
149         disposables +=
150             view.repeatWhenAttached(mainImmediateDispatcher) {
151                 repeatOnLifecycle(Lifecycle.State.CREATED) {
152                     launch("$TAG#topClippingBounds") {
153                         val clipBounds = Rect()
154                         viewModel.topClippingBounds.collect { clipTop ->
155                             if (clipTop == null) {
156                                 view.setClipBounds(null)
157                             } else {
158                                 clipBounds.apply {
159                                     top = clipTop
160                                     left = view.getLeft()
161                                     right = view.getRight()
162                                     bottom = view.getBottom()
163                                 }
164                                 view.setClipBounds(clipBounds)
165                             }
166                         }
167                     }
168 
169                     launch("$TAG#alpha") {
170                         viewModel.alpha(viewState).collect { alpha ->
171                             view.alpha = alpha
172                             childViews[burnInLayerId]?.alpha = alpha
173                         }
174                     }
175 
176                     launch("$TAG#zoomOut") {
177                         viewModel.scaleFromZoomOut.collect { scaleFromZoomOut ->
178                             view.scaleX = scaleFromZoomOut
179                             view.scaleY = scaleFromZoomOut
180                         }
181                     }
182 
183                     launch("$TAG#translationY") {
184                         // When translation happens in burnInLayer, it won't be weather clock large
185                         // clock isn't added to burnInLayer due to its scale transition so we also
186                         // need to add translation to it here same as translationX
187                         viewModel.translationY.collect { y ->
188                             childViews[burnInLayerId]?.translationY = y
189                             childViews[largeClockId]?.translationY = y
190                             if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
191                                 childViews[largeClockDateId]?.translationY = y
192                             }
193                             childViews[aodPromotedNotificationId]?.translationY = y
194                             childViews[aodNotificationIconContainerId]?.translationY = y
195                         }
196                     }
197 
198                     launch("$TAG#translationX") {
199                         viewModel.translationX.collect { state ->
200                             val px = state.value ?: return@collect
201                             when {
202                                 state.isToOrFrom(KeyguardState.AOD) -> {
203                                     // Large Clock is not translated in the x direction
204                                     childViews[burnInLayerId]?.translationX = px
205                                     childViews[aodPromotedNotificationId]?.translationX = px
206                                     childViews[aodNotificationIconContainerId]?.translationX = px
207                                 }
208 
209                                 state.isToOrFrom(KeyguardState.GLANCEABLE_HUB) -> {
210                                     for ((key, childView) in childViews.entries) {
211                                         when (key) {
212                                             indicationArea,
213                                             startButton,
214                                             endButton,
215                                             deviceEntryIcon -> {
216                                                 // Do not move these views
217                                             }
218 
219                                             else -> childView.translationX = px
220                                         }
221                                     }
222                                 }
223                             }
224                         }
225                     }
226                 }
227             }
228         disposables +=
229             view.repeatWhenAttached {
230                 repeatOnLifecycle(Lifecycle.State.CREATED) {
231                     if (SceneContainerFlag.isEnabled) {
232                         view.setViewTreeOnBackPressedDispatcherOwner(
233                             object : OnBackPressedDispatcherOwner {
234                                 override val onBackPressedDispatcher =
235                                     OnBackPressedDispatcher().apply {
236                                         setOnBackInvokedDispatcher(
237                                             view.viewRootImpl.onBackInvokedDispatcher
238                                         )
239                                     }
240 
241                                 override val lifecycle: Lifecycle =
242                                     this@repeatWhenAttached.lifecycle
243                             }
244                         )
245                     }
246                     launch {
247                         occludingAppDeviceEntryMessageViewModel?.message?.collect { biometricMessage
248                             ->
249                             if (biometricMessage?.message != null) {
250                                 chipbarCoordinator!!.displayView(
251                                     createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
252                                 )
253                             } else {
254                                 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
255                             }
256                         }
257                     }
258 
259                     launch {
260                         viewModel.burnInLayerVisibility.collect { visibility ->
261                             childViews[burnInLayerId]?.visibility = visibility
262                         }
263                     }
264 
265                     launch {
266                         viewModel.scale.collect { scaleViewModel ->
267                             if (scaleViewModel.scaleClockOnly) {
268                                 // For clocks except weather clock, we have scale transition besides
269                                 // translate
270                                 childViews[largeClockId]?.let {
271                                     it.scaleX = scaleViewModel.scale
272                                     it.scaleY = scaleViewModel.scale
273                                 }
274                             }
275                         }
276                     }
277 
278                     launch {
279                         blueprintViewModel.currentTransition.collect { currentTransition ->
280                             // When blueprint/clock transitions end (null), make sure NSSL is in the
281                             // right place
282                             if (currentTransition == null) {
283                                 childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
284                                     viewModel.onNotificationContainerBoundsChanged(
285                                         notificationListPlaceholder.top.toFloat(),
286                                         notificationListPlaceholder.bottom.toFloat(),
287                                         animate = true,
288                                     )
289                                 }
290                             }
291                         }
292                     }
293 
294                     launch {
295                         val iconsAppearTranslationPx =
296                             configuration
297                                 .getDimensionPixelSize(R.dimen.shelf_appear_translation)
298                                 .stateIn(this)
299                         viewModel.isNotifIconContainerVisible.collect { isVisible ->
300                             if (isVisible.value) {
301                                 blueprintViewModel.refreshBlueprint()
302                             }
303                             childViews[aodNotificationIconContainerId]
304                                 ?.setAodNotifIconContainerIsVisible(isVisible)
305                         }
306                     }
307 
308                     launch {
309                         viewModel.isAodPromotedNotifVisible.collect { isVisible ->
310                             if (isVisible.value) {
311                                 blueprintViewModel.refreshBlueprint()
312                             }
313                             childViews[aodPromotedNotificationId]?.setAodPromotedNotifIsVisible(
314                                 isVisible
315                             )
316                         }
317                     }
318 
319                     launch {
320                         shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded ->
321                             view.visibility =
322                                 if (isFullyAnyExpanded) {
323                                     INVISIBLE
324                                 } else {
325                                     VISIBLE
326                                 }
327                         }
328                     }
329 
330                     launch { burnInParams.collect { viewModel.updateBurnInParams(it) } }
331 
332                     if (deviceEntryHapticsInteractor != null && vibratorHelper != null) {
333                         launch {
334                             deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect {
335                                 if (msdlFeedback()) {
336                                     msdlPlayer?.playToken(
337                                         MSDLToken.UNLOCK,
338                                         authInteractionProperties,
339                                     )
340                                 } else {
341                                     vibratorHelper.performHapticFeedback(
342                                         view,
343                                         HapticFeedbackConstants.BIOMETRIC_CONFIRM,
344                                     )
345                                 }
346                             }
347                         }
348 
349                         launch {
350                             deviceEntryHapticsInteractor.playErrorHaptic.collect {
351                                 if (msdlFeedback()) {
352                                     msdlPlayer?.playToken(
353                                         MSDLToken.FAILURE,
354                                         authInteractionProperties,
355                                     )
356                                 } else {
357                                     vibratorHelper.performHapticFeedback(
358                                         view,
359                                         HapticFeedbackConstants.BIOMETRIC_REJECT,
360                                     )
361                                 }
362                             }
363                         }
364                     }
365                 }
366             }
367 
368         burnInParams.update { current ->
369             current.copy(
370                 translationX = { childViews[burnInLayerId]?.translationX },
371                 translationY = { childViews[burnInLayerId]?.translationY },
372             )
373         }
374 
375         disposables +=
376             view.repeatWhenAttached {
377                 repeatOnLifecycle(Lifecycle.State.STARTED) {
378                     if (wallpaperFocalAreaViewModel.hasFocalArea.value) {
379                         launch {
380                             wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect {
381                                 wallpaperFocalAreaViewModel.setFocalAreaBounds(it)
382                             }
383                         }
384                     }
385                 }
386             }
387 
388         disposables +=
389             view.onLayoutChanged(
390                 OnLayoutChange(
391                     viewModel,
392                     blueprintViewModel,
393                     smartspaceViewModel,
394                     childViews,
395                     burnInParams,
396                     Logger(blueprintLog, TAG),
397                 )
398             )
399 
400         // Views will be added or removed after the call to bind(). This is needed to avoid many
401         // calls to findViewById
402         view.setOnHierarchyChangeListener(
403             object : OnHierarchyChangeListener {
404                 override fun onChildViewAdded(parent: View, child: View) {
405                     childViews.put(child.id, child)
406                 }
407 
408                 override fun onChildViewRemoved(parent: View, child: View) {
409                     childViews.remove(child.id)
410                 }
411             }
412         )
413         disposables += DisposableHandle {
414             view.setOnHierarchyChangeListener(null)
415             childViews.clear()
416         }
417 
418         disposables +=
419             view.onApplyWindowInsets { _: View, insets: WindowInsets ->
420                 val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
421                 burnInParams.update { current ->
422                     current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
423                 }
424                 insets
425             }
426 
427         return disposables
428     }
429 
430     /**
431      * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
432      */
433     private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
434         return ChipbarInfo(
435             startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
436             text = Text.Loaded(message),
437             endItem = null,
438             vibrationEffect = null,
439             windowTitle = "OccludingAppUnlockMsgChip",
440             wakeReason = "OCCLUDING_APP_UNLOCK_MSG_CHIP",
441             timeoutMs = 3500,
442             id = ID,
443             priority = ViewPriority.CRITICAL,
444             instanceId = null,
445         )
446     }
447 
448     private class OnLayoutChange(
449         private val viewModel: KeyguardRootViewModel,
450         private val blueprintViewModel: KeyguardBlueprintViewModel,
451         private val smartspaceViewModel: KeyguardSmartspaceViewModel,
452         private val childViews: Map<Int, View>,
453         private val burnInParams: MutableStateFlow<BurnInParameters>,
454         private val logger: Logger,
455     ) : OnLayoutChangeListener {
456         var prevTransition: TransitionData? = null
457 
458         override fun onLayoutChange(
459             view: View,
460             left: Int,
461             top: Int,
462             right: Int,
463             bottom: Int,
464             oldLeft: Int,
465             oldTop: Int,
466             oldRight: Int,
467             oldBottom: Int,
468         ) {
469             val prevSmartspaceVisibility = smartspaceViewModel.bcSmartspaceVisibility.value
470             val smartspaceVisibility = childViews[bcSmartspaceId]?.visibility ?: GONE
471             val smartspaceVisibilityChanged = prevSmartspaceVisibility != smartspaceVisibility
472 
473             // After layout, ensure the notifications are positioned correctly
474             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
475                 // Do not update a second time while a blueprint transition is running
476                 val transition = blueprintViewModel.currentTransition.value
477                 val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
478                 if (prevTransition == transition && shouldAnimate && !smartspaceVisibilityChanged) {
479                     logger.w("Skipping onNotificationContainerBoundsChanged during transition")
480                     return
481                 }
482 
483                 prevTransition = transition
484                 viewModel.onNotificationContainerBoundsChanged(
485                     notificationListPlaceholder.top.toFloat(),
486                     notificationListPlaceholder.bottom.toFloat(),
487                     animate = (shouldAnimate || smartspaceVisibilityChanged),
488                 )
489             }
490 
491             burnInParams.update { current ->
492                 current.copy(
493                     minViewY =
494                         // To ensure burn-in doesn't enroach the top inset, get the min top Y
495                         childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
496                             min(
497                                 currentMin,
498                                 if (!isUserVisible(view)) {
499                                     Int.MAX_VALUE
500                                 } else {
501                                     view.getTop()
502                                 },
503                             )
504                         }
505                 )
506             }
507         }
508 
509         private fun isUserVisible(view: View): Boolean {
510             return view.id != burnInLayerId &&
511                 view.visibility == VISIBLE &&
512                 view.width > 0 &&
513                 view.height > 0
514         }
515     }
516 
517     private fun View.setAodNotifIconContainerIsVisible(isVisible: AnimatedValue<Boolean>) {
518         animate().cancel()
519         val animatorListener =
520             object : AnimatorListenerAdapter() {
521                 override fun onAnimationEnd(animation: Animator) {
522                     isVisible.stopAnimating()
523                 }
524             }
525         when {
526             !isVisible.isAnimating -> {
527                 visibility =
528                     if (isVisible.value) {
529                         alpha = 1f
530                         VISIBLE
531                     } else {
532                         alpha = 0f
533                         INVISIBLE
534                     }
535             }
536 
537             else -> {
538                 if (isVisible.value) {
539                     CrossFadeHelper.fadeIn(this, animatorListener)
540                 } else {
541                     CrossFadeHelper.fadeOut(this, animatorListener)
542                 }
543             }
544         }
545     }
546 
547     private fun View.setAodPromotedNotifIsVisible(isVisible: AnimatedValue<Boolean>) {
548         animate().cancel()
549         val animatorListener =
550             object : AnimatorListenerAdapter() {
551                 override fun onAnimationEnd(animation: Animator) {
552                     isVisible.stopAnimating()
553                 }
554             }
555 
556         if (isVisible.isAnimating) {
557             if (isVisible.value) {
558                 alpha = 0f
559                 visibility = VISIBLE
560                 CrossFadeHelper.fadeIn(this, animatorListener)
561             } else {
562                 CrossFadeHelper.fadeOut(this, animatorListener)
563             }
564         } else {
565             if (isVisible.value) {
566                 alpha = 1f
567                 visibility = VISIBLE
568             } else {
569                 // Hide with GONE, not INVISIBLE, so there won't be a redundant bottom
570                 // margin between the smart space and the shelf.
571                 alpha = 0f
572                 visibility = GONE
573             }
574         }
575     }
576 
577     private fun MotionEvent.isTouchscreenSource(): Boolean {
578         return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true
579     }
580 
581     private val burnInLayerId = R.id.burn_in_layer
582     private val aodPromotedNotificationId = AodPromotedNotificationSection.viewId
583     private val aodNotificationIconContainerId = R.id.aod_notification_icon_container
584     private val largeClockId = customR.id.lockscreen_clock_view_large
585     private val largeClockDateId = sharedR.id.date_smartspace_view_large
586     private val largeClockWeatherId = sharedR.id.weather_smartspace_view_large
587     private val bcSmartspaceId = sharedR.id.bc_smartspace_view
588     private val smallClockId = customR.id.lockscreen_clock_view
589     private val indicationArea = R.id.keyguard_indication_area
590     private val startButton = R.id.start_button
591     private val endButton = R.id.end_button
592     private val deviceEntryIcon = R.id.device_entry_icon_view
593     private val nsslPlaceholderId = R.id.nssl_placeholder
594     private val authInteractionProperties = AuthInteractionProperties()
595 
596     private const val ID = "occluding_app_device_entry_unlock_msg"
597     private const val AOD_ICONS_APPEAR_DURATION: Long = 200
598     private const val TAG = "KeyguardRootViewBinder"
599 }
600