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

Commit 4dc8eacf authored by Beverly's avatar Beverly
Browse files

Add OccludingAppDeviceEntryInteractor

Handles proceeding to the home screen on fingerprint
success.

Updates `message` when a new message should show
over an occluding app over the lockscreen

Test: atest OccludignAppDeviceEntryInteractorTest
Bug: 288308594
Change-Id: I0d29774956529bed7cc577ac9fc8eb7e49a2e249
parent ea2ffe4a
Loading
Loading
Loading
Loading
+138 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.domain.interactor

import android.content.Context
import android.content.Intent
import android.hardware.fingerprint.FingerprintManager
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.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch

/** Business logic for handling authentication events when an app is occluding the lockscreen. */
@ExperimentalCoroutinesApi
@SysUISingleton
class OccludingAppDeviceEntryInteractor
@Inject
constructor(
    biometricMessageInteractor: BiometricMessageInteractor,
    fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
    keyguardInteractor: KeyguardInteractor,
    primaryBouncerInteractor: PrimaryBouncerInteractor,
    alternateBouncerInteractor: AlternateBouncerInteractor,
    @Application scope: CoroutineScope,
    private val context: Context,
    activityStarter: ActivityStarter,
) {
    private val keyguardOccludedByApp: Flow<Boolean> =
        combine(
                keyguardInteractor.isKeyguardOccluded,
                keyguardInteractor.isKeyguardShowing,
                primaryBouncerInteractor.isShowing,
                alternateBouncerInteractor.isVisible,
            ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
                occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
            }
            .distinctUntilChanged()
    private val fingerprintUnlockSuccessEvents: Flow<Unit> =
        fingerprintAuthRepository.authenticationStatus
            .ifKeyguardOccludedByApp()
            .filter { it is SuccessFingerprintAuthenticationStatus }
            .map {} // maps FingerprintAuthenticationStatus => Unit
    private val fingerprintLockoutEvents: Flow<Unit> =
        fingerprintAuthRepository.authenticationStatus
            .ifKeyguardOccludedByApp()
            .filter {
                it is ErrorFingerprintAuthenticationStatus &&
                    (it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT ||
                        it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
            }
            .map {} // maps FingerprintAuthenticationStatus => Unit
    val message: Flow<BiometricMessage?> =
        merge(
                biometricMessageInteractor.fingerprintErrorMessage,
                biometricMessageInteractor.fingerprintFailMessage,
                biometricMessageInteractor.fingerprintHelpMessage,
            )
            .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))

    init {
        scope.launch {
            // On fingerprint success, go to the home screen
            fingerprintUnlockSuccessEvents.collect { goToHomeScreen() }
        }

        scope.launch {
            // On device fingerprint lockout, request the bouncer with a runnable to
            // go to the home screen. Without this, the bouncer won't proceed to the home screen.
            fingerprintLockoutEvents.collect {
                activityStarter.dismissKeyguardThenExecute(
                    object : ActivityStarter.OnDismissAction {
                        override fun onDismiss(): Boolean {
                            goToHomeScreen()
                            return false
                        }

                        override fun willRunAnimationOnKeyguard(): Boolean {
                            return false
                        }
                    },
                    /* cancel= */ null,
                    /* afterKeyguardGone */ false
                )
            }
        }
    }

    /** Launches an Activity which forces the current app to background by going home. */
    private fun goToHomeScreen() {
        context.startActivity(
            Intent(Intent.ACTION_MAIN).apply {
                addCategory(Intent.CATEGORY_HOME)
                flags = Intent.FLAG_ACTIVITY_NEW_TASK
            }
        )
    }

    private fun <T> Flow<T>.ifKeyguardOccludedByApp(elseFlow: Flow<T> = emptyFlow()): Flow<T> {
        return keyguardOccludedByApp.flatMapLatest { keyguardOccludedByApp ->
            if (keyguardOccludedByApp) {
                this
            } else {
                elseFlow
            }
        }
    }
}
+299 −0
Original line number Diff line number Diff line
/*
 *  Copyright (C) 2023 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.domain.interactor

import android.content.Context
import android.content.Intent
import android.hardware.biometrics.BiometricSourceType
import android.hardware.fingerprint.FingerprintManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.keyguard.util.IndicationHelper
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
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.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() {

    private lateinit var underTest: OccludingAppDeviceEntryInteractor
    private lateinit var testScope: TestScope
    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
    private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository
    private lateinit var keyguardRepository: FakeKeyguardRepository
    private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
    private lateinit var configurationRepository: FakeConfigurationRepository
    private lateinit var featureFlags: FakeFeatureFlags
    private lateinit var trustRepository: FakeTrustRepository

    @Mock private lateinit var indicationHelper: IndicationHelper
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var mockedContext: Context
    @Mock private lateinit var activityStarter: ActivityStarter

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        testScope = TestScope()
        biometricSettingsRepository = FakeBiometricSettingsRepository()
        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
        fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
        keyguardRepository = FakeKeyguardRepository()
        bouncerRepository = FakeKeyguardBouncerRepository()
        configurationRepository = FakeConfigurationRepository()
        featureFlags =
            FakeFeatureFlags().apply {
                set(Flags.FACE_AUTH_REFACTOR, false)
                set(Flags.DELAY_BOUNCER, false)
            }
        trustRepository = FakeTrustRepository()
        underTest =
            OccludingAppDeviceEntryInteractor(
                BiometricMessageInteractor(
                    mContext.resources,
                    fingerprintAuthRepository,
                    fingerprintPropertyRepository,
                    indicationHelper,
                    keyguardUpdateMonitor,
                ),
                fingerprintAuthRepository,
                KeyguardInteractor(
                    keyguardRepository,
                    commandQueue = mock(),
                    featureFlags,
                    bouncerRepository,
                    configurationRepository,
                ),
                PrimaryBouncerInteractor(
                    bouncerRepository,
                    primaryBouncerView = mock(),
                    mainHandler = mock(),
                    keyguardStateController = mock(),
                    keyguardSecurityModel = mock(),
                    primaryBouncerCallbackInteractor = mock(),
                    falsingCollector = mock(),
                    dismissCallbackRegistry = mock(),
                    context,
                    keyguardUpdateMonitor,
                    trustRepository,
                    featureFlags,
                    testScope.backgroundScope,
                ),
                AlternateBouncerInteractor(
                    statusBarStateController = mock(),
                    keyguardStateController = mock(),
                    bouncerRepository,
                    biometricSettingsRepository,
                    FakeSystemClock(),
                    keyguardUpdateMonitor,
                ),
                testScope.backgroundScope,
                mockedContext,
                activityStarter,
            )
    }

    @Test
    fun fingerprintSuccess_goToHomeScreen() =
        testScope.runTest {
            givenOnOccludingApp(true)
            fingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
            verifyGoToHomeScreen()
        }

    @Test
    fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() =
        testScope.runTest {
            givenOnOccludingApp(false)
            fingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
            verifyNeverGoToHomeScreen()
        }

    @Test
    fun lockout_goToHomeScreenOnDismissAction() =
        testScope.runTest {
            givenOnOccludingApp(true)
            fingerprintAuthRepository.setAuthenticationStatus(
                ErrorFingerprintAuthenticationStatus(
                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
                    "lockoutTest"
                )
            )
            runCurrent()
            verifyGoToHomeScreenOnDismiss()
        }

    @Test
    fun lockout_notOnOccludingApp_neverGoToHomeScreen() =
        testScope.runTest {
            givenOnOccludingApp(false)
            fingerprintAuthRepository.setAuthenticationStatus(
                ErrorFingerprintAuthenticationStatus(
                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
                    "lockoutTest"
                )
            )
            runCurrent()
            verifyNeverGoToHomeScreen()
        }

    @Test
    fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() =
        testScope.runTest {
            val message by collectLastValue(underTest.message)

            givenOnOccludingApp(true)
            givenPrimaryAuthRequired(false)
            runCurrent()
            // WHEN a fp failure come in
            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
            // THEN message set to failure
            assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL)

            // GIVEN fingerprint shouldn't run
            givenOnOccludingApp(false)
            runCurrent()
            // WHEN another fp failure arrives
            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)

            // THEN message set to null
            assertThat(message).isNull()
        }

    @Test
    fun message_fpErrorHelpFailOnOccludingApp() =
        testScope.runTest {
            val message by collectLastValue(underTest.message)

            givenOnOccludingApp(true)
            givenPrimaryAuthRequired(false)
            runCurrent()

            // ERROR message
            fingerprintAuthRepository.setAuthenticationStatus(
                ErrorFingerprintAuthenticationStatus(
                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
                    "testError",
                )
            )
            assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
            assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)
            assertThat(message?.message).isEqualTo("testError")
            assertThat(message?.type).isEqualTo(BiometricMessageType.ERROR)

            // HELP message
            fingerprintAuthRepository.setAuthenticationStatus(
                HelpFingerprintAuthenticationStatus(
                    FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL,
                    "testHelp",
                )
            )
            assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
            assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL)
            assertThat(message?.message).isEqualTo("testHelp")
            assertThat(message?.type).isEqualTo(BiometricMessageType.HELP)

            // FAIL message
            fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
            assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
            assertThat(message?.id)
                .isEqualTo(KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
            assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL)
        }

    private fun givenOnOccludingApp(isOnOccludingApp: Boolean) {
        keyguardRepository.setKeyguardOccluded(isOnOccludingApp)
        keyguardRepository.setKeyguardShowing(isOnOccludingApp)
        bouncerRepository.setPrimaryShow(!isOnOccludingApp)
        bouncerRepository.setAlternateVisible(!isOnOccludingApp)
    }

    private fun givenPrimaryAuthRequired(required: Boolean) {
        whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
            .thenReturn(!required)
    }

    private fun verifyGoToHomeScreen() {
        val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
        verify(mockedContext).startActivity(intentCaptor.capture())

        assertThat(intentCaptor.value.hasCategory(Intent.CATEGORY_HOME)).isTrue()
        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MAIN)
    }

    private fun verifyNeverGoToHomeScreen() {
        verify(mockedContext, never()).startActivity(any())
        verify(activityStarter, never())
            .dismissKeyguardThenExecute(any(OnDismissAction::class.java), isNull(), eq(false))
    }

    private fun verifyGoToHomeScreenOnDismiss() {
        val onDimissActionCaptor = ArgumentCaptor.forClass(OnDismissAction::class.java)
        verify(activityStarter)
            .dismissKeyguardThenExecute(onDimissActionCaptor.capture(), isNull(), eq(false))
        onDimissActionCaptor.value.onDismiss()

        verifyGoToHomeScreen()
    }
}