Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1a374b83 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Password bouncer beahvior fixes." into main

parents 054df310 75e427af
Loading
Loading
Loading
Loading
+50 −19
Original line number Diff line number Diff line
@@ -16,12 +16,10 @@

package com.android.systemui.bouncer.ui.composable

import android.view.ViewTreeObserver
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
@@ -30,46 +28,56 @@ import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowInsetsCompat
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel

/** UI for the input part of a password-requiring version of the bouncer. */
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun PasswordBouncer(
    viewModel: PasswordBouncerViewModel,
    modifier: Modifier = Modifier,
) {
    val focusRequester = remember { FocusRequester() }
    val isTextFieldFocusRequested by viewModel.isTextFieldFocusRequested.collectAsState()
    LaunchedEffect(isTextFieldFocusRequested) {
        if (isTextFieldFocusRequested) {
            focusRequester.requestFocus()
        }
    }
    val (isTextFieldFocused, onTextFieldFocusChanged) = remember { mutableStateOf(false) }
    LaunchedEffect(isTextFieldFocused) {
        viewModel.onTextFieldFocusChanged(isFocused = isTextFieldFocused)
    }

    val password: String by viewModel.password.collectAsState()
    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()

    val density = LocalDensity.current
    val isImeVisible by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density) > 0)
    val isImeVisible by isSoftwareKeyboardVisible()
    LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) }

    DisposableEffect(Unit) {
        viewModel.onShown()

        // When the UI comes up, request focus on the TextField to bring up the software keyboard.
        focusRequester.requestFocus()

        onDispose { viewModel.onHidden() }
    }

@@ -104,7 +112,9 @@ internal fun PasswordBouncer(
                    onDone = { viewModel.onAuthenticateKeyPressed() },
                ),
            modifier =
                Modifier.focusRequester(focusRequester).drawBehind {
                Modifier.focusRequester(focusRequester)
                    .onFocusChanged { onTextFieldFocusChanged(it.isFocused) }
                    .drawBehind {
                        drawLine(
                            color = color,
                            start = Offset(x = 0f, y = size.height - lineWidthPx),
@@ -117,3 +127,24 @@ internal fun PasswordBouncer(
        Spacer(Modifier.height(100.dp))
    }
}

/** Returns a [State] with `true` when the IME/keyboard is visible and `false` when it's not. */
@Composable
fun isSoftwareKeyboardVisible(): State<Boolean> {
    val view = LocalView.current
    val viewTreeObserver = view.viewTreeObserver

    return produceState(
        initialValue = false,
        key1 = viewTreeObserver,
    ) {
        val listener =
            ViewTreeObserver.OnGlobalLayoutListener {
                value = view.rootWindowInsets?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
            }

        viewTreeObserver.addOnGlobalLayoutListener(listener)

        awaitDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) }
    }
}
+6 −6
Original line number Diff line number Diff line
@@ -105,9 +105,9 @@ constructor(
    val isUserSwitcherVisible: Boolean
        get() = repository.isUserSwitcherVisible

    private val _onImeHidden = MutableSharedFlow<Unit>()
    /** Provide the onImeHidden events from the bouncer */
    val onImeHidden: SharedFlow<Unit> = _onImeHidden
    private val _onImeHiddenByUser = MutableSharedFlow<Unit>()
    /** Emits a [Unit] each time the IME (keyboard) is hidden by the user. */
    val onImeHiddenByUser: SharedFlow<Unit> = _onImeHiddenByUser

    init {
        if (flags.isEnabled()) {
@@ -230,9 +230,9 @@ constructor(
        repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
    }

    /** Notifies the interactor that the input method editor has been hidden. */
    suspend fun onImeHidden() {
        _onImeHidden.emit(Unit)
    /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
    suspend fun onImeHiddenByUser() {
        _onImeHiddenByUser.emit(Unit)
    }

    private fun promptMessage(authMethod: AuthenticationMethodModel): String {
+1 −16
Original line number Diff line number Diff line
@@ -46,9 +46,6 @@ sealed class AuthMethodBouncerViewModel(
     */
    val animateFailure: StateFlow<Boolean> = _animateFailure.asStateFlow()

    /** Whether the input method editor (for example, the software keyboard) is visible. */
    private var isImeVisible: Boolean = false

    /** The authentication method that corresponds to this view model. */
    abstract val authenticationMethod: AuthenticationMethodModel

@@ -68,7 +65,7 @@ sealed class AuthMethodBouncerViewModel(
    /**
     * Notifies that the UI has been hidden from the user (after any transitions have completed).
     */
    fun onHidden() {
    open fun onHidden() {
        clearInput()
        interactor.resetMessage()
    }
@@ -78,18 +75,6 @@ sealed class AuthMethodBouncerViewModel(
        interactor.onDown()
    }

    /**
     * Notifies that the input method editor (for example, the software keyboard) has been shown or
     * hidden.
     */
    suspend fun onImeVisibilityChanged(isVisible: Boolean) {
        if (isImeVisible && !isVisible) {
            interactor.onImeHidden()
        }

        isImeVisible = isVisible
    }

    /**
     * Notifies that the failure animation has been shown. This should be called to consume a `true`
     * value in [animateFailure].
+46 −0
Original line number Diff line number Diff line
@@ -21,8 +21,11 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.res.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn

/** Holds UI state and handles user input for the password bouncer UI. */
class PasswordBouncerViewModel(
@@ -45,6 +48,32 @@ class PasswordBouncerViewModel(

    override val throttlingMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message

    /** Whether the input method editor (for example, the software keyboard) is visible. */
    private var isImeVisible: Boolean = false

    /** Whether the text field element currently has focus. */
    private val isTextFieldFocused = MutableStateFlow(false)

    /** Whether the UI should request focus on the text field element. */
    val isTextFieldFocusRequested =
        combine(
                interactor.isThrottled,
                isTextFieldFocused,
            ) { isThrottled, hasFocus ->
                !isThrottled && !hasFocus
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = !interactor.isThrottled.value && !isTextFieldFocused.value,
            )

    override fun onHidden() {
        super.onHidden()
        isImeVisible = false
        isTextFieldFocused.value = false
    }

    override fun clearInput() {
        _password.value = ""
    }
@@ -72,4 +101,21 @@ class PasswordBouncerViewModel(
            tryAuthenticate()
        }
    }

    /**
     * Notifies that the input method editor (for example, the software keyboard) has been shown or
     * hidden.
     */
    suspend fun onImeVisibilityChanged(isVisible: Boolean) {
        if (isImeVisible && !isVisible && !interactor.isThrottled.value) {
            interactor.onImeHiddenByUser()
        }

        isImeVisible = isVisible
    }

    /** Notifies that the password text field has gained or lost focus. */
    fun onTextFieldFocusChanged(isFocused: Boolean) {
        isTextFieldFocused.value = isFocused
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -128,7 +128,7 @@ constructor(
    private fun automaticallySwitchScenes() {
        applicationScope.launch {
            // TODO (b/308001302): Move this to a bouncer specific interactor.
            bouncerInteractor.onImeHidden.collectLatest {
            bouncerInteractor.onImeHiddenByUser.collectLatest {
                if (sceneInteractor.desiredScene.value.key == SceneKey.Bouncer) {
                    sceneInteractor.changeScene(
                        scene = SceneModel(SceneKey.Lockscreen),
Loading