Allow using a hardware keyboard to unlock the app using a pin code (#4530)
* Allow using a hardware keyboard to unlock the app using a pin code * Add UI tests to `PinKeypad` * Also take into account the numpad keys. Extract this to an extension property in `ui-utils`. Made `ui-utils` also a compose-compatible library (vs `android-utils`, which doesn't have compose dependencies).
This commit is contained in:
parent
b3c0332eac
commit
915699a265
5 changed files with 201 additions and 3 deletions
|
|
@ -27,6 +27,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceIn
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -37,6 +43,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.digit
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -60,7 +68,22 @@ fun PinKeypad(
|
|||
val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally)
|
||||
val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically)
|
||||
Column(
|
||||
modifier = modifier,
|
||||
modifier = modifier.onKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyUp) {
|
||||
val digitChar = event.digit
|
||||
if (digitChar != null) {
|
||||
onClick(PinKeypadModel.Number(digitChar))
|
||||
true
|
||||
} else if (event.key == Key.Backspace) {
|
||||
onClick(PinKeypadModel.Back)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
verticalArrangement = verticalArrangement,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
) {
|
||||
|
|
@ -183,7 +206,7 @@ private fun PinKeypadBackButton(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Backspace,
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(CommonStrings.a11y_delete),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock.keypad
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isRoot
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performKeyInput
|
||||
import androidx.compose.ui.test.pressKey
|
||||
import androidx.compose.ui.test.requestFocus
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class PinKeypadTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on a number emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<PinKeypadModel>()
|
||||
rule.setPinKeyPad(onClick = eventsRecorder)
|
||||
rule.onNode(hasText("1")).performClick()
|
||||
eventsRecorder.assertSingle(PinKeypadModel.Number('1'))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on the delete previous character button emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<PinKeypadModel>()
|
||||
rule.setPinKeyPad(onClick = eventsRecorder)
|
||||
rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.a11y_delete))).performClick()
|
||||
eventsRecorder.assertSingle(PinKeypadModel.Back)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
@Test
|
||||
fun `typing using the hardware keyboard emits the expected events`() {
|
||||
val eventsRecorder = EventsRecorder<PinKeypadModel>()
|
||||
rule.setPinKeyPad(onClick = eventsRecorder)
|
||||
rule.onNodeWithText("1").requestFocus()
|
||||
rule.onAllNodes(isRoot())[0].performKeyInput {
|
||||
val keys = listOf(
|
||||
Key.A,
|
||||
Key.NumPad1,
|
||||
Key.NumPad2,
|
||||
Key.NumPad3,
|
||||
Key.NumPad4,
|
||||
Key.NumPad5,
|
||||
Key.NumPad6,
|
||||
Key.NumPad7,
|
||||
Key.NumPad8,
|
||||
Key.NumPad9,
|
||||
Key.NumPad0,
|
||||
Key(KeyEvent.KEYCODE_1),
|
||||
Key(KeyEvent.KEYCODE_2),
|
||||
Key(KeyEvent.KEYCODE_3),
|
||||
Key(KeyEvent.KEYCODE_4),
|
||||
Key(KeyEvent.KEYCODE_5),
|
||||
Key(KeyEvent.KEYCODE_6),
|
||||
Key(KeyEvent.KEYCODE_7),
|
||||
Key(KeyEvent.KEYCODE_8),
|
||||
Key(KeyEvent.KEYCODE_9),
|
||||
Key(KeyEvent.KEYCODE_0),
|
||||
Key.Backspace,
|
||||
)
|
||||
for (key in keys) {
|
||||
pressKey(key)
|
||||
}
|
||||
}
|
||||
eventsRecorder.assertList(
|
||||
listOf(
|
||||
// Note that the first key is not a number, but a letter so it's ignored as input
|
||||
// Then we have the numpad keys
|
||||
PinKeypadModel.Number('1'),
|
||||
PinKeypadModel.Number('2'),
|
||||
PinKeypadModel.Number('3'),
|
||||
PinKeypadModel.Number('4'),
|
||||
PinKeypadModel.Number('5'),
|
||||
PinKeypadModel.Number('6'),
|
||||
PinKeypadModel.Number('7'),
|
||||
PinKeypadModel.Number('8'),
|
||||
PinKeypadModel.Number('9'),
|
||||
PinKeypadModel.Number('0'),
|
||||
// And the normal keys from the number row in the keyboard
|
||||
PinKeypadModel.Number('1'),
|
||||
PinKeypadModel.Number('2'),
|
||||
PinKeypadModel.Number('3'),
|
||||
PinKeypadModel.Number('4'),
|
||||
PinKeypadModel.Number('5'),
|
||||
PinKeypadModel.Number('6'),
|
||||
PinKeypadModel.Number('7'),
|
||||
PinKeypadModel.Number('8'),
|
||||
PinKeypadModel.Number('9'),
|
||||
PinKeypadModel.Number('0'),
|
||||
PinKeypadModel.Back,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinKeyPad(
|
||||
onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
PinKeypad(
|
||||
onClick = onClick,
|
||||
maxWidth = 1000.dp,
|
||||
maxHeight = 1000.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue