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