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

Commit 34926ef6 authored by Beverly's avatar Beverly
Browse files

[flexiglass] AlternateBouncer support

The AlternateBouncer shows as its owe distinct UI
above the Lockscreen and Shade scenes when the
scene container flag is enabled.

Add AlternateBouncer over the shade logic to drive
the transition to the AlternateBouncer showing over
the Shade/Notifications/Quick Settings.

Ensure AlternateBouncer can show over occluding
apps by updating SceneContainerStartable#hydrateVisibility
to make SceneContainer visible when altBouncer is visible.

Flag: com.android.systemui.scene_container
Bug: 353955910
Test: with UDFPS or SFPS enrolled, expand the shade on the
lockscreen and tap on a notification that requires auth;
observe the AlternateBouncer shows and user can auth
from this screen. User can also tap the scrim for
the primary bouncer.
Test: atest AlternateBouncerViewModelTest

Change-Id: Ia9b60737d55c4fd0f670773173eedd12066e546d
parent c78fba16
Loading
Loading
Loading
Loading
+198 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyguard.ui.composable

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.modifiers.background
import com.android.compose.modifiers.height
import com.android.compose.modifiers.width
import com.android.systemui.deviceentry.shared.model.BiometricMessage
import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
import com.android.systemui.keyguard.ui.binder.AlternateBouncerUdfpsViewBinder
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
import com.android.systemui.res.R
import kotlinx.coroutines.ExperimentalCoroutinesApi

@ExperimentalCoroutinesApi
@Composable
fun AlternateBouncer(
    alternateBouncerDependencies: AlternateBouncerDependencies,
    modifier: Modifier = Modifier,
) {

    val isVisible by
        alternateBouncerDependencies.viewModel.isVisible.collectAsStateWithLifecycle(
            initialValue = false
        )

    val udfpsIconLocation by
        alternateBouncerDependencies.udfpsIconViewModel.iconLocation.collectAsStateWithLifecycle(
            initialValue = null
        )

    // TODO (b/353955910): back handling doesn't work
    BackHandler { alternateBouncerDependencies.viewModel.onBackRequested() }

    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(),
        exit = fadeOut(),
        modifier = modifier,
    ) {
        Box(
            contentAlignment = Alignment.TopCenter,
            modifier =
                Modifier.background(color = Colors.AlternateBouncerBackgroundColor, alpha = { 1f })
                    .pointerInput(Unit) {
                        detectTapGestures(
                            onTap = { alternateBouncerDependencies.viewModel.onTapped() }
                        )
                    },
        ) {
            StatusMessage(
                viewModel = alternateBouncerDependencies.messageAreaViewModel,
            )
        }

        udfpsIconLocation?.let { udfpsLocation ->
            Box {
                DeviceEntryIcon(
                    viewModel = alternateBouncerDependencies.udfpsIconViewModel,
                    modifier =
                        Modifier.width { udfpsLocation.width }
                            .height { udfpsLocation.height }
                            .fillMaxHeight()
                            .offset {
                                IntOffset(
                                    x = udfpsLocation.left,
                                    y = udfpsLocation.top,
                                )
                            },
                )
            }

            UdfpsA11yOverlay(
                viewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel.get(),
                modifier = Modifier.fillMaxHeight(),
            )
        }
    }
}

@ExperimentalCoroutinesApi
@Composable
private fun StatusMessage(
    viewModel: AlternateBouncerMessageAreaViewModel,
    modifier: Modifier = Modifier,
) {
    val message: BiometricMessage? by
        viewModel.message.collectAsStateWithLifecycle(initialValue = null)

    Crossfade(
        targetState = message,
        label = "Alternate Bouncer message",
        animationSpec = tween(),
        modifier = modifier,
    ) { biometricMessage ->
        biometricMessage?.let {
            Text(
                textAlign = TextAlign.Center,
                text = it.message ?: "",
                color = Colors.AlternateBouncerTextColor,
                fontSize = 18.sp,
                lineHeight = 24.sp,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.padding(top = 92.dp),
            )
        }
    }
}

@ExperimentalCoroutinesApi
@Composable
private fun DeviceEntryIcon(
    viewModel: AlternateBouncerUdfpsIconViewModel,
    modifier: Modifier = Modifier,
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            val view =
                DeviceEntryIconView(context, null).apply {
                    id = R.id.alternate_bouncer_udfps_icon_view
                    contentDescription =
                        context.resources.getString(R.string.accessibility_fingerprint_label)
                }
            AlternateBouncerUdfpsViewBinder.bind(view, viewModel)
            view
        },
    )
}

/** TODO (b/353955910): Validate accessibility CUJs */
@ExperimentalCoroutinesApi
@Composable
private fun UdfpsA11yOverlay(
    viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel,
    modifier: Modifier = Modifier,
) {
    AndroidView(
        factory = { context ->
            val view =
                UdfpsAccessibilityOverlay(context).apply {
                    id = R.id.alternate_bouncer_udfps_accessibility_overlay
                }
            UdfpsAccessibilityOverlayBinder.bind(view, viewModel)
            view
        },
        modifier = modifier,
    )
}

private object Colors {
    val AlternateBouncerBackgroundColor: Color = Color.Black.copy(alpha = .66f)
    val AlternateBouncerTextColor: Color = Color.White
}
+6 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import kotlinx.coroutines.ExperimentalCoroutinesApi

@ExperimentalCoroutinesApi
@@ -52,9 +53,13 @@ object AlternateBouncerUdfpsViewBinder {
                    }
                }

                if (SceneContainerFlag.isEnabled) {
                    view.alpha = 1f
                } else {
                    launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } }
                }
            }
        }

        fgIconView.repeatWhenAttached {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
+14 −14
Original line number Diff line number Diff line
@@ -37,13 +37,13 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scrim.ScrimView
import dagger.Lazy
import javax.inject.Inject
@@ -67,7 +67,6 @@ constructor(
    private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>,
    private val windowManager: Lazy<WindowManager>,
    private val layoutInflater: Lazy<LayoutInflater>,
    private val dismissCallbackRegistry: DismissCallbackRegistry,
) : CoreStartable {
    private val layoutParams: WindowManager.LayoutParams
        get() =
@@ -95,9 +94,10 @@ constructor(
    private var alternateBouncerView: ConstraintLayout? = null

    override fun start() {
        if (!DeviceEntryUdfpsRefactor.isEnabled) {
        if (!DeviceEntryUdfpsRefactor.isEnabled || SceneContainerFlag.isEnabled) {
            return
        }

        applicationScope.launch("$TAG#alternateBouncerWindowViewModel") {
            alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect {
                addAlternateBouncerWindowView ->
@@ -110,7 +110,7 @@ constructor(
                    bind(alternateBouncerView!!, alternateBouncerDependencies.get())
                } else {
                    removeViewFromWindowManager()
                    alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
                    alternateBouncerDependencies.get().viewModel.onRemovedFromWindow()
                }
            }
        }
@@ -144,7 +144,7 @@ constructor(
    private val onAttachAddBackGestureHandler =
        object : View.OnAttachStateChangeListener {
            private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback {
                onBackRequested()
                alternateBouncerDependencies.get().viewModel.onBackRequested()
            }

            override fun onViewAttachedToWindow(view: View) {
@@ -161,14 +161,12 @@ constructor(
                    .findOnBackInvokedDispatcher()
                    ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
            }

            fun onBackRequested() {
                alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
                dismissCallbackRegistry.notifyDismissCancelled()
            }
        }

    private fun addViewToWindowManager() {
        if (SceneContainerFlag.isEnabled) {
            return
        }
        if (alternateBouncerView != null) {
            return
        }
@@ -190,6 +188,7 @@ constructor(
        if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
            return
        }

        optionallyAddUdfpsViews(
            view = view,
            udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel,
@@ -202,12 +201,13 @@ constructor(
            viewModel = alternateBouncerDependencies.messageAreaViewModel,
        )

        val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView
        val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim)
        val viewModel = alternateBouncerDependencies.viewModel
        val swipeUpAnywhereGestureHandler =
            alternateBouncerDependencies.swipeUpAnywhereGestureHandler
        val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector
        view.repeatWhenAttached { alternateBouncerViewContainer ->

        view.repeatWhenAttached {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch("$TAG#viewModel.registerForDismissGestures") {
                        viewModel.registerForDismissGestures.collect { registerForDismissGestures ->
@@ -216,11 +216,11 @@ constructor(
                                    swipeTag
                                ) { _ ->
                                    alternateBouncerDependencies.powerInteractor.onUserTouch()
                                    viewModel.showPrimaryBouncer()
                                    viewModel.onTapped()
                                }
                                tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ ->
                                    alternateBouncerDependencies.powerInteractor.onUserTouch()
                                    viewModel.showPrimaryBouncer()
                                    viewModel.onTapped()
                                }
                            } else {
                                swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback(
+2 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shared.recents.utilities.Utilities.clamp
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -50,6 +51,7 @@ constructor(
    private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
    val alpha: Flow<Float> =
        alternateBouncerViewModel.transitionToAlternateBouncerProgress.map {
            SceneContainerFlag.assertInLegacyMode()
            clamp(it * 2f, 0f, 1f)
        }

+19 −3
Original line number Diff line number Diff line
@@ -18,15 +18,20 @@
package com.android.systemui.keyguard.ui.viewmodel

import android.graphics.Color
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

@ExperimentalCoroutinesApi
class AlternateBouncerViewModel
@@ -34,12 +39,18 @@ class AlternateBouncerViewModel
constructor(
    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
    keyguardTransitionInteractor: KeyguardTransitionInteractor,
    private val dismissCallbackRegistry: DismissCallbackRegistry,
    alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>,
) {
    // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be:
    private val alternateBouncerScrimAlpha = .66f

    /** Reports the alternate bouncer visible state if the scene container flag is enabled. */
    val isVisible: Flow<Boolean> =
        alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.assertInNewMode() }

    /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */
    val transitionToAlternateBouncerProgress =
    val transitionToAlternateBouncerProgress: Flow<Float> =
        keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER)

    /** An observable for the scrim alpha. */
@@ -51,11 +62,16 @@ constructor(
    val registerForDismissGestures: Flow<Boolean> =
        transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged()

    fun showPrimaryBouncer() {
    fun onTapped() {
        statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
    }

    fun hideAlternateBouncer() {
    fun onRemovedFromWindow() {
        statusBarKeyguardViewManager.hideAlternateBouncer(false)
    }

    fun onBackRequested() {
        statusBarKeyguardViewManager.hideAlternateBouncer(false)
        dismissCallbackRegistry.notifyDismissCancelled()
    }
}
Loading