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

Commit deb07988 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Fixes issue where bouncer exited upon orientation change.

Previous logic was set up to leave the bouncer scene when the IME
(software keyboard) became hidden. Unfortunately, this was the wrong
signal as it would happen also when changing device configuration (for
example, when changing device orientation).

A better (and less complex) approach is shown in this CL, we listen for
the back navigation keyevent from the IME.

Fix: 322518343
Test: manually verified that changing orientations doesn't dismiss the
bouncer
Test: unit tests updated
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT

Change-Id: I53b6c2c605d3621ce78247305cbe3a900d2ca3fd
parent fe75d5cd
Loading
Loading
Loading
Loading
+14 −29
Original line number Diff line number Diff line
@@ -14,9 +14,10 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalComposeUiApi::class)

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

import android.view.ViewTreeObserver
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
@@ -25,25 +26,25 @@ 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.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
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.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
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. */
@@ -64,9 +65,6 @@ internal fun PasswordBouncer(
    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsState()

    val isImeVisible by isSoftwareKeyboardVisible()
    LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) }

    DisposableEffect(Unit) {
        viewModel.onShown()
        onDispose { viewModel.onHidden() }
@@ -109,27 +107,14 @@ internal fun PasswordBouncer(
                        end = Offset(size.width, y = size.height - lineWidthPx),
                        strokeWidth = lineWidthPx,
                    )
                },
    )
                }

/** 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) }
                .onInterceptKeyBeforeSoftKeyboard { keyEvent ->
                    if (keyEvent.key == Key.Back) {
                        viewModel.onImeDismissed()
                        true
                    } else {
                        false
                    }
                },
    )
}
+2 −34
Original line number Diff line number Diff line
@@ -208,45 +208,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
        }

    @Test
    fun onImeVisibilityChanged_false_doesNothing() =
    fun onImeDismissed() =
        testScope.runTest {
            val events by collectValues(bouncerInteractor.onImeHiddenByUser)
            assertThat(events).isEmpty()

            underTest.onImeVisibilityChanged(isVisible = false)
            assertThat(events).isEmpty()
        }

    @Test
    fun onImeVisibilityChanged_falseAfterTrue_emitsOnImeHiddenByUserEvent() =
        testScope.runTest {
            val events by collectValues(bouncerInteractor.onImeHiddenByUser)
            assertThat(events).isEmpty()

            underTest.onImeVisibilityChanged(isVisible = true)
            assertThat(events).isEmpty()

            underTest.onImeVisibilityChanged(isVisible = false)
            assertThat(events).hasSize(1)

            underTest.onImeVisibilityChanged(isVisible = true)
            underTest.onImeDismissed()
            assertThat(events).hasSize(1)

            underTest.onImeVisibilityChanged(isVisible = false)
            assertThat(events).hasSize(2)
        }

    @Test
    fun onImeVisibilityChanged_falseAfterTrue_whileLockedOut_doesNothing() =
        testScope.runTest {
            val events by collectValues(bouncerInteractor.onImeHiddenByUser)
            assertThat(events).isEmpty()
            underTest.onImeVisibilityChanged(isVisible = true)
            setLockout(true)

            underTest.onImeVisibilityChanged(isVisible = false)

            assertThat(events).isEmpty()
        }

    @Test
+2 −7
Original line number Diff line number Diff line
@@ -776,14 +776,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
    }

    /** Emulates the dismissal of the IME (soft keyboard). */
    private suspend fun TestScope.dismissIme(
        showImeBeforeDismissing: Boolean = true,
    ) {
    private fun TestScope.dismissIme() {
        (bouncerViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
            if (showImeBeforeDismissing) {
                it.onImeVisibilityChanged(true)
            }
            it.onImeVisibilityChanged(false)
            it.onImeDismissed()
            runCurrent()
        }
    }
+4 −14
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

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

    override val lockoutMessageId = 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)

@@ -65,7 +63,6 @@ class PasswordBouncerViewModel(

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

@@ -97,16 +94,9 @@ class PasswordBouncerViewModel(
        }
    }

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

        isImeVisible = isVisible
    /** Notifies that the user has dismissed the software keyboard (IME). */
    fun onImeDismissed() {
        viewModelScope.launch { interactor.onImeHiddenByUser() }
    }

    /** Notifies that the password text field has gained or lost focus. */