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 @file:OptIn(ExperimentalComposeUiApi::class)
18
19 package com.android.systemui.bouncer.ui.composable
20
21 import androidx.compose.foundation.text.KeyboardActions
22 import androidx.compose.foundation.text.KeyboardOptions
23 import androidx.compose.material3.IconButtonDefaults
24 import androidx.compose.material3.LocalTextStyle
25 import androidx.compose.material3.MaterialTheme
26 import androidx.compose.material3.TextField
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.DisposableEffect
29 import androidx.compose.runtime.LaunchedEffect
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.remember
32 import androidx.compose.ui.ExperimentalComposeUiApi
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.draw.drawBehind
35 import androidx.compose.ui.focus.FocusRequester
36 import androidx.compose.ui.focus.focusRequester
37 import androidx.compose.ui.focus.onFocusChanged
38 import androidx.compose.ui.geometry.Offset
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.input.key.Key
41 import androidx.compose.ui.input.key.key
42 import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
43 import androidx.compose.ui.platform.LocalContext
44 import androidx.compose.ui.platform.LocalDensity
45 import androidx.compose.ui.res.stringResource
46 import androidx.compose.ui.text.input.ImeAction
47 import androidx.compose.ui.text.input.KeyboardType
48 import androidx.compose.ui.text.input.PasswordVisualTransformation
49 import androidx.compose.ui.text.style.TextAlign
50 import androidx.compose.ui.unit.dp
51 import androidx.lifecycle.compose.collectAsStateWithLifecycle
52 import com.android.compose.PlatformIconButton
53 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
54 import com.android.systemui.common.ui.compose.SelectedUserAwareInputConnection
55 import com.android.systemui.compose.modifiers.sysuiResTag
56 import com.android.systemui.res.R
57
58 /** UI for the input part of a password-requiring version of the bouncer. */
59 @Composable
60 internal fun PasswordBouncer(
61 viewModel: PasswordBouncerViewModel,
62 modifier: Modifier = Modifier,
63 ) {
64 val focusRequester = remember { FocusRequester() }
65 val isTextFieldFocusRequested by
66 viewModel.isTextFieldFocusRequested.collectAsStateWithLifecycle()
67 LaunchedEffect(isTextFieldFocusRequested) {
68 if (isTextFieldFocusRequested) {
69 focusRequester.requestFocus()
70 }
71 }
72
73 val password: String by viewModel.password.collectAsStateWithLifecycle()
74 val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle()
75 val animateFailure: Boolean by viewModel.animateFailure.collectAsStateWithLifecycle()
76 val isImeSwitcherButtonVisible by
77 viewModel.isImeSwitcherButtonVisible.collectAsStateWithLifecycle()
78 val selectedUserId by viewModel.selectedUserId.collectAsStateWithLifecycle()
79
80 DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
81
82 LaunchedEffect(animateFailure) {
83 if (animateFailure) {
84 // We don't currently have a failure animation for password, just consume it:
85 viewModel.onFailureAnimationShown()
86 }
87 }
88
89 val color = MaterialTheme.colorScheme.onSurfaceVariant
90 val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
91
92 SelectedUserAwareInputConnection(selectedUserId) {
93 TextField(
94 value = password,
95 onValueChange = viewModel::onPasswordInputChanged,
96 enabled = isInputEnabled,
97 visualTransformation = PasswordVisualTransformation(),
98 singleLine = true,
99 textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
100 keyboardOptions =
101 KeyboardOptions(
102 keyboardType = KeyboardType.Password,
103 imeAction = ImeAction.Done,
104 ),
105 keyboardActions =
106 KeyboardActions(
107 onDone = { viewModel.onAuthenticateKeyPressed() },
108 ),
109 modifier =
110 modifier
111 .sysuiResTag("bouncer_text_entry")
112 .focusRequester(focusRequester)
113 .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
114 .drawBehind {
115 drawLine(
116 color = color,
117 start = Offset(x = 0f, y = size.height - lineWidthPx),
118 end = Offset(size.width, y = size.height - lineWidthPx),
119 strokeWidth = lineWidthPx,
120 )
121 }
122 .onInterceptKeyBeforeSoftKeyboard { keyEvent ->
123 if (keyEvent.key == Key.Back) {
124 viewModel.onImeDismissed()
125 true
126 } else {
127 false
128 }
129 },
130 trailingIcon =
131 if (isImeSwitcherButtonVisible) {
132 { ImeSwitcherButton(viewModel, color) }
133 } else {
134 null
135 }
136 )
137 }
138 }
139
140 /** Button for changing the password input method (IME). */
141 @Composable
ImeSwitcherButtonnull142 private fun ImeSwitcherButton(
143 viewModel: PasswordBouncerViewModel,
144 color: Color,
145 ) {
146 val context = LocalContext.current
147 PlatformIconButton(
148 onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
149 iconResource = R.drawable.ic_lockscreen_ime,
150 contentDescription = stringResource(R.string.accessibility_ime_switch_button),
151 colors =
152 IconButtonDefaults.filledIconButtonColors(
153 contentColor = color,
154 containerColor = Color.Transparent,
155 )
156 )
157 }
158