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

Commit 89761285 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] AlternateBouncer support" into main

parents 86fc59fa 34926ef6
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