• 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 @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