• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.composable
18 
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.Crossfade
21 import androidx.compose.animation.core.MutableTransitionState
22 import androidx.compose.animation.core.tween
23 import androidx.compose.animation.fadeIn
24 import androidx.compose.animation.fadeOut
25 import androidx.compose.foundation.background
26 import androidx.compose.foundation.gestures.detectTapGestures
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.fillMaxHeight
29 import androidx.compose.foundation.layout.offset
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.material3.Text
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.LaunchedEffect
34 import androidx.compose.runtime.getValue
35 import androidx.compose.runtime.remember
36 import androidx.compose.ui.Alignment
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.graphics.Color
39 import androidx.compose.ui.input.pointer.pointerInput
40 import androidx.compose.ui.text.style.TextAlign
41 import androidx.compose.ui.text.style.TextOverflow
42 import androidx.compose.ui.unit.IntOffset
43 import androidx.compose.ui.unit.dp
44 import androidx.compose.ui.unit.sp
45 import androidx.compose.ui.viewinterop.AndroidView
46 import androidx.lifecycle.compose.collectAsStateWithLifecycle
47 import com.android.compose.modifiers.height
48 import com.android.compose.modifiers.width
49 import com.android.systemui.deviceentry.shared.model.BiometricMessage
50 import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
51 import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
52 import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
53 import com.android.systemui.keyguard.ui.binder.AlternateBouncerUdfpsViewBinder
54 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
55 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
56 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
57 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
58 import com.android.systemui.log.TouchHandlingViewLogger
59 import com.android.systemui.res.R
60 
61 @Composable
62 fun AlternateBouncer(
63     alternateBouncerDependencies: AlternateBouncerDependencies,
64     onHideAnimationFinished: () -> Unit,
65     modifier: Modifier = Modifier,
66 ) {
67 
68     val isVisible by
69         alternateBouncerDependencies.viewModel.isVisible.collectAsStateWithLifecycle(true)
70     val visibleState = remember { MutableTransitionState(isVisible) }
71 
72     // Feeds the isVisible value to the MutableTransitionState used by AnimatedVisibility below.
73     LaunchedEffect(isVisible) { visibleState.targetState = isVisible }
74 
75     // Watches the MutableTransitionState and calls onHideAnimationFinished when the fade out
76     // animation is finished. This way the window view is removed from the view hierarchy only after
77     // the fade out animation is complete.
78     LaunchedEffect(visibleState.currentState, visibleState.isIdle) {
79         if (!visibleState.currentState && visibleState.isIdle) {
80             onHideAnimationFinished()
81         }
82     }
83 
84     val udfpsIconLocation by
85         alternateBouncerDependencies.udfpsIconViewModel.iconLocation.collectAsStateWithLifecycle(
86             initialValue = null
87         )
88 
89     AnimatedVisibility(
90         visibleState = visibleState,
91         enter = fadeIn(),
92         exit = fadeOut(),
93         modifier = modifier,
94     ) {
95         Box(
96             contentAlignment = Alignment.TopCenter,
97             modifier =
98                 Modifier.background(color = Colors.AlternateBouncerBackgroundColor).pointerInput(
99                     Unit
100                 ) {
101                     detectTapGestures(onTap = { alternateBouncerDependencies.viewModel.onTapped() })
102                 },
103         ) {
104             StatusMessage(viewModel = alternateBouncerDependencies.messageAreaViewModel)
105         }
106 
107         udfpsIconLocation?.let { udfpsLocation ->
108             Box {
109                 DeviceEntryIcon(
110                     viewModel = alternateBouncerDependencies.udfpsIconViewModel,
111                     logger = alternateBouncerDependencies.logger,
112                     modifier =
113                         Modifier.width { udfpsLocation.width }
114                             .height { udfpsLocation.height }
115                             .fillMaxHeight()
116                             .offset { IntOffset(x = udfpsLocation.left, y = udfpsLocation.top) },
117                 )
118             }
119 
120             UdfpsA11yOverlay(
121                 viewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel.get(),
122                 modifier = Modifier.fillMaxHeight(),
123             )
124         }
125     }
126 }
127 
128 @Composable
StatusMessagenull129 private fun StatusMessage(
130     viewModel: AlternateBouncerMessageAreaViewModel,
131     modifier: Modifier = Modifier,
132 ) {
133     val message: BiometricMessage? by
134         viewModel.message.collectAsStateWithLifecycle(initialValue = null)
135 
136     Crossfade(
137         targetState = message,
138         label = "Alternate Bouncer message",
139         animationSpec = tween(),
140         modifier = modifier,
141     ) { biometricMessage ->
142         biometricMessage?.let {
143             Text(
144                 textAlign = TextAlign.Center,
145                 text = it.message ?: "",
146                 color = Colors.AlternateBouncerTextColor,
147                 fontSize = 18.sp,
148                 lineHeight = 24.sp,
149                 overflow = TextOverflow.Ellipsis,
150                 modifier = Modifier.padding(top = 92.dp),
151             )
152         }
153     }
154 }
155 
156 @Composable
DeviceEntryIconnull157 private fun DeviceEntryIcon(
158     viewModel: AlternateBouncerUdfpsIconViewModel,
159     logger: TouchHandlingViewLogger,
160     modifier: Modifier = Modifier,
161 ) {
162     AndroidView(
163         modifier = modifier,
164         factory = { context ->
165             val view =
166                 DeviceEntryIconView(context, null, logger = logger).apply {
167                     id = R.id.alternate_bouncer_udfps_icon_view
168                     contentDescription =
169                         context.resources.getString(R.string.accessibility_fingerprint_label)
170                 }
171             AlternateBouncerUdfpsViewBinder.bind(view, viewModel)
172             view
173         },
174     )
175 }
176 
177 /** TODO (b/353955910): Validate accessibility CUJs */
178 @Composable
UdfpsA11yOverlaynull179 private fun UdfpsA11yOverlay(
180     viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel,
181     modifier: Modifier = Modifier,
182 ) {
183     AndroidView(
184         factory = { context ->
185             val view =
186                 UdfpsAccessibilityOverlay(context).apply {
187                     id = R.id.alternate_bouncer_udfps_accessibility_overlay
188                 }
189             UdfpsAccessibilityOverlayBinder.bind(view, viewModel)
190             view
191         },
192         modifier = modifier,
193     )
194 }
195 
196 private object Colors {
197     val AlternateBouncerBackgroundColor: Color = Color.Black.copy(alpha = .66f)
198     val AlternateBouncerTextColor: Color = Color.White
199 }
200