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

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

Merge "Only register for face auth lift gesture when device is interactive" into main

parents 9d6a9b7f c82878ee
Loading
Loading
Loading
Loading
+200 −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.deviceentry.domain.ui.binder

import android.content.packageManager
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.TriggerEventListener
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
import com.android.systemui.deviceentry.ui.binder.liftToRunFaceAuthBinder
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.sensors.asyncSensorManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class LiftToRunFaceAuthBinderTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val sensorManager = kosmos.asyncSensorManager
    private val powerRepository = kosmos.fakePowerRepository
    private val keyguardRepository = kosmos.fakeKeyguardRepository
    private val bouncerRepository = kosmos.keyguardBouncerRepository
    private val biometricSettingsRepository = kosmos.biometricSettingsRepository
    private val packageManager = kosmos.packageManager

    @Captor private lateinit var triggerEventListenerCaptor: ArgumentCaptor<TriggerEventListener>
    @Mock private lateinit var mockSensor: Sensor

    private val underTest = kosmos.liftToRunFaceAuthBinder

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
        whenever(sensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)).thenReturn(mockSensor)
    }

    @Test
    fun doNotListenForGesture() =
        testScope.runTest {
            start()
            verifyNeverRequestsTriggerSensor()
        }

    @Test
    fun awakeKeyguard_listenForGesture() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(true)
            runCurrent()
            verifyRequestTriggerSensor()
        }

    @Test
    fun faceNotEnrolled_listenForGesture() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(true)
            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
            runCurrent()
            verifyNeverRequestsTriggerSensor()
        }

    @Test
    fun notInteractive_doNotListenForGesture() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(true)
            powerRepository.setInteractive(false)
            runCurrent()
            verifyNeverRequestsTriggerSensor()
        }

    @Test
    fun primaryBouncer_listenForGesture() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(false)
            givenPrimaryBouncerShowing()
            runCurrent()
            verifyRequestTriggerSensor()
        }

    @Test
    fun alternateBouncer_listenForGesture() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(false)
            givenAlternateBouncerShowing()
            runCurrent()
            verifyRequestTriggerSensor()
        }

    @Test
    fun restartListeningForGestureAfterSensorTrigger() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(true)
            runCurrent()
            verifyRequestTriggerSensor()
            clearInvocations(sensorManager)

            triggerEventListenerCaptor.value.onTrigger(null)
            runCurrent()
            verifyRequestTriggerSensor()
        }

    @Test
    fun cancelTriggerSensor_keyguardNotAwakeAnymore() =
        testScope.runTest {
            start()
            givenAwakeKeyguard(true)
            runCurrent()
            verifyRequestTriggerSensor()

            givenAwakeKeyguard(false)
            runCurrent()
            verifyCancelTriggerSensor()
        }

    private fun start() {
        underTest.start()
        biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
        givenAwakeKeyguard(false)
        givenBouncerNotShowing()
    }

    private fun givenAwakeKeyguard(isAwake: Boolean) {
        powerRepository.setInteractive(isAwake)
        keyguardRepository.setKeyguardShowing(isAwake)
        keyguardRepository.setKeyguardOccluded(false)
    }

    private fun givenPrimaryBouncerShowing() {
        bouncerRepository.setPrimaryShow(true)
        bouncerRepository.setAlternateVisible(false)
    }

    private fun givenBouncerNotShowing() {
        bouncerRepository.setPrimaryShow(false)
        bouncerRepository.setAlternateVisible(false)
    }

    private fun givenAlternateBouncerShowing() {
        bouncerRepository.setPrimaryShow(false)
        bouncerRepository.setAlternateVisible(true)
    }

    private fun verifyRequestTriggerSensor() {
        verify(sensorManager).requestTriggerSensor(capture(triggerEventListenerCaptor), any())
    }

    private fun verifyNeverRequestsTriggerSensor() {
        verify(sensorManager, never()).requestTriggerSensor(any(), any())
    }

    private fun verifyCancelTriggerSensor() {
        verify(sensorManager).cancelTriggerSensor(any(), any())
    }
}
+3 −5
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -52,11 +52,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() {
    val testScope = kosmos.testScope

    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
    val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
    val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
    val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
    val underTest by lazy {
        kosmos.primaryBouncerToGoneTransitionViewModel
    }
    val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel }

    @Before
    fun setUp() {
+0 −7
Original line number Diff line number Diff line
@@ -51,7 +51,6 @@ import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.gesture.GesturePointerEventListener
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener
import com.android.systemui.stylus.StylusUsiPowerStartable
@@ -225,12 +224,6 @@ abstract class SystemUICoreStartableModule {
    @ClassKey(WMShell::class)
    abstract fun bindWMShell(sysui: WMShell): CoreStartable

    /** Inject into KeyguardLiftController.  */
    @Binds
    @IntoMap
    @ClassKey(KeyguardLiftController::class)
    abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable

    /** Inject into MediaTttSenderCoordinator. */
    @Binds
    @IntoMap
+143 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 * 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.
@@ -11,112 +11,127 @@
 * 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
 * limitations under the License.
 */

package com.android.systemui.statusbar.phone
package com.android.systemui.deviceentry.ui.binder

import android.content.Context
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
import com.android.keyguard.ActiveUnlockConfig
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.util.Assert
import com.android.systemui.util.sensors.AsyncSensorManager
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

/**
 * Triggers face auth on lift when the device is showing the lock screen. Only initialized
 * if face auth is supported on the device. Not to be confused with the lift to wake gesture
 * which is handled by {@link com.android.server.policy.PhoneWindowManager}.
 * Triggers face auth and active unlock on lift when the device is showing the lock screen or
 * bouncer. Only initialized if face auth is supported on the device. Not to be confused with the
 * lift to wake gesture which is handled by {@link com.android.server.policy.PhoneWindowManager}.
 */
@SysUISingleton
class KeyguardLiftController @Inject constructor(
        private val context: Context,
        private val statusBarStateController: StatusBarStateController,
class LiftToRunFaceAuthBinder
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val packageManager: PackageManager,
    private val asyncSensorManager: AsyncSensorManager,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
    keyguardInteractor: KeyguardInteractor,
    primaryBouncerInteractor: PrimaryBouncerInteractor,
    alternateBouncerInteractor: AlternateBouncerInteractor,
    private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
        private val dumpManager: DumpManager,
        private val selectedUserInteractor: SelectedUserInteractor,
) : Dumpable, CoreStartable {
    powerInteractor: PowerInteractor,
) : CoreStartable {

    private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
    private var isListening = false
    private var bouncerVisible = false
    private var pickupSensor: Sensor? = null
    private val isListening: MutableStateFlow<Boolean> = MutableStateFlow(false)
    private val stoppedListening: Flow<Unit> = isListening.filterNot { it }.map {} // map to Unit

    private val onAwakeKeyguard: Flow<Boolean> =
        combine(
            powerInteractor.isInteractive,
            keyguardInteractor.isKeyguardVisible,
        ) { isInteractive, isKeyguardVisible ->
            isInteractive && isKeyguardVisible
        }
    private val bouncerShowing: Flow<Boolean> =
        combine(
            primaryBouncerInteractor.isShowing,
            alternateBouncerInteractor.isVisible,
        ) { primaryBouncerShowing, alternateBouncerShowing ->
            primaryBouncerShowing || alternateBouncerShowing
        }
    private val listenForPickupSensor: Flow<Boolean> =
        combine(
            stoppedListening,
            bouncerShowing,
            onAwakeKeyguard,
        ) { _, bouncerShowing, onAwakeKeyguard ->
            (onAwakeKeyguard || bouncerShowing) &&
                deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
        }

    override fun start() {
        if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
        if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
            init()
        }
    }

    private fun init() {
        dumpManager.registerDumpable(this)
        statusBarStateController.addCallback(statusBarStateListener)
        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
        updateListeningState()
        pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
        scope.launch {
            listenForPickupSensor.collect { listenForPickupSensor ->
                updateListeningState(listenForPickupSensor)
            }
        }
    }

    private val listener: TriggerEventListener = object : TriggerEventListener() {
    private val listener: TriggerEventListener =
        object : TriggerEventListener() {
            override fun onTrigger(event: TriggerEvent?) {
                Assert.isMainThread()
            // Not listening anymore since trigger events unregister themselves
            isListening = false
            updateListeningState()
                deviceEntryFaceAuthInteractor.onDeviceLifted()
                keyguardUpdateMonitor.requestActiveUnlock(
                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
                "KeyguardLiftController")
        }
    }
                    "KeyguardLiftController"
                )

    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
        override fun onKeyguardBouncerFullyShowingChanged(bouncer: Boolean) {
            bouncerVisible = bouncer
            updateListeningState()
        }

        override fun onKeyguardVisibilityChanged(visible: Boolean) {
            updateListeningState()
        }
    }

    private val statusBarStateListener = object : StatusBarStateController.StateListener {
        override fun onDozingChanged(isDozing: Boolean) {
            updateListeningState()
                // Not listening anymore since trigger events unregister themselves
                isListening.value = false
            }
        }

    override fun dump(pw: PrintWriter, args: Array<out String>) {
        pw.println("KeyguardLiftController:")
        pw.println("LiftToRunFaceAuthBinder:")
        pw.println("  pickupSensor: $pickupSensor")
        pw.println("  isListening: $isListening")
        pw.println("  bouncerVisible: $bouncerVisible")
        pw.println("  isListening: ${isListening.value}")
    }

    private fun updateListeningState() {
    private fun updateListeningState(shouldListen: Boolean) {
        if (pickupSensor == null) {
            return
        }
        val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible &&
                !statusBarStateController.isDozing

        val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
        val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled
        if (shouldListen != isListening) {
            isListening = shouldListen
        if (shouldListen != isListening.value) {
            isListening.value = shouldListen

            if (shouldListen) {
                asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
+2 −2
Original line number Diff line number Diff line
@@ -51,7 +51,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule;
import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
@@ -104,7 +104,7 @@ import kotlinx.coroutines.CoroutineDispatcher;
            FalsingModule.class,
            KeyguardDataQuickAffordanceModule.class,
            KeyguardRepositoryModule.class,
            KeyguardFaceAuthModule.class,
            DeviceEntryFaceAuthModule.class,
            KeyguardDisplayModule.class,
            StartKeyguardTransitionModule.class,
            ResourceTrimmerModule.class,
Loading