Introduce bouncer delay behind flag

Whenever the flag is enabled, the 250ms bouncer
delay is enabled as long as face auth is enrolled
and could run or if active unlock (regardless of whether
it's actually available) could've run based on the device
state.

This doesn't yet use the true active unlock available signal yet
since the API isn't available.

Test: atest PrimaryBouncerInteractorTest
Bug: 267322286
Change-Id: I1f87d2e4848f112880dad64995309ed384407b63
This commit is contained in:
Beverly
2023-04-27 18:20:13 +00:00
committed by Beverly Tai
parent 348bb0e22c
commit b585614729
12 changed files with 170 additions and 58 deletions

View File

@@ -889,4 +889,8 @@
-->
<dimen name="shade_swipe_collapse_threshold">0.5</dimen>
<!-- [END] MULTI SHADE -->
<!-- Time (in ms) to delay the bouncer views from showing when passive auth may be used for
device entry. -->
<integer name="primary_bouncer_passive_auth_delay">250</integer>
</resources>

View File

@@ -2884,7 +2884,19 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
}
/**
* If the current state of the device allows for triggering active unlock. This does not
* include active unlock availability.
*/
public boolean canTriggerActiveUnlockBasedOnDeviceState() {
return shouldTriggerActiveUnlock(/* shouldLog */ false);
}
private boolean shouldTriggerActiveUnlock() {
return shouldTriggerActiveUnlock(/* shouldLog */ true);
}
private boolean shouldTriggerActiveUnlock(boolean shouldLog) {
// Triggers:
final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mAlternateBouncerShowing
@@ -2914,19 +2926,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
&& !mKeyguardGoingAway
&& !mSecureCameraLaunched;
// Aggregate relevant fields for debug logging.
logListenerModelData(
new KeyguardActiveUnlockModel(
System.currentTimeMillis(),
user,
shouldTriggerActiveUnlock,
awakeKeyguard,
mAuthInterruptActive,
fpLockedOut,
primaryAuthRequired,
mSwitchingUser,
triggerActiveUnlockForAssistant,
userCanDismissLockScreen));
if (shouldLog) {
// Aggregate relevant fields for debug logging.
logListenerModelData(
new KeyguardActiveUnlockModel(
System.currentTimeMillis(),
user,
shouldTriggerActiveUnlock,
awakeKeyguard,
mAuthInterruptActive,
fpLockedOut,
primaryAuthRequired,
mSwitchingUser,
triggerActiveUnlockForAssistant,
userCanDismissLockScreen));
}
return shouldTriggerActiveUnlock;
}

View File

@@ -46,8 +46,6 @@ interface KeyguardFaceAuthModule {
impl: SystemUIKeyguardFaceAuthInteractor
): KeyguardFaceAuthInteractor
@Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository
companion object {
@Provides
@SysUISingleton

View File

@@ -45,4 +45,6 @@ interface KeyguardRepositoryModule {
@Binds
fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
@Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository
}

View File

@@ -28,7 +28,9 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@@ -40,6 +42,9 @@ import kotlinx.coroutines.flow.shareIn
interface TrustRepository {
/** Flow representing whether the current user is trusted. */
val isCurrentUserTrusted: Flow<Boolean>
/** Flow representing whether active unlock is available for the current user. */
val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean>
}
@SysUISingleton
@@ -89,11 +94,13 @@ constructor(
}
.shareIn(applicationScope, started = SharingStarted.Eagerly, replay = 1)
override val isCurrentUserTrusted: Flow<Boolean>
get() =
combine(trust, userRepository.selectedUserInfo, ::Pair)
.map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
.distinctUntilChanged()
.onEach { logger.isCurrentUserTrusted(it) }
.onStart { emit(false) }
override val isCurrentUserTrusted: Flow<Boolean> =
combine(trust, userRepository.selectedUserInfo, ::Pair)
.map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
.distinctUntilChanged()
.onEach { logger.isCurrentUserTrusted(it) }
.onStart { emit(false) }
// TODO: Implement based on TrustManager callback b/267322286
override val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean> = MutableStateFlow(true)
}

View File

@@ -32,14 +32,16 @@ import com.android.systemui.R
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -56,28 +58,21 @@ import javax.inject.Inject
class PrimaryBouncerInteractor
@Inject
constructor(
private val repository: KeyguardBouncerRepository,
private val primaryBouncerView: BouncerView,
@Main private val mainHandler: Handler,
private val keyguardStateController: KeyguardStateController,
private val keyguardSecurityModel: KeyguardSecurityModel,
private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
private val falsingCollector: FalsingCollector,
private val dismissCallbackRegistry: DismissCallbackRegistry,
private val context: Context,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
keyguardBypassController: KeyguardBypassController,
private val repository: KeyguardBouncerRepository,
private val primaryBouncerView: BouncerView,
@Main private val mainHandler: Handler,
private val keyguardStateController: KeyguardStateController,
private val keyguardSecurityModel: KeyguardSecurityModel,
private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
private val falsingCollector: FalsingCollector,
private val dismissCallbackRegistry: DismissCallbackRegistry,
private val context: Context,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val trustRepository: TrustRepository,
private val featureFlags: FeatureFlags,
) {
/** Whether we want to wait for face auth. */
private val primaryBouncerFaceDelay =
keyguardStateController.isFaceAuthEnabled &&
!keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
KeyguardUpdateMonitor.getCurrentUser()
) &&
!needsFullscreenBouncer() &&
keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE) &&
!keyguardBypassController.bypassEnabled
private val passiveAuthBouncerDelay = context.resources.getInteger(
R.integer.primary_bouncer_passive_auth_delay).toLong()
/** Runnable to show the primary bouncer. */
val showRunnable = Runnable {
repository.setPrimaryShow(true)
@@ -160,8 +155,9 @@ constructor(
}
repository.setPrimaryShowingSoon(true)
if (primaryBouncerFaceDelay) {
mainHandler.postDelayed(showRunnable, 1200L)
if (usePrimaryBouncerPassiveAuthDelay()) {
Log.d(TAG, "delay bouncer, passive auth may succeed")
mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
} else {
DejankUtils.postAfterTraversal(showRunnable)
}
@@ -377,6 +373,17 @@ constructor(
return repository.primaryBouncerShow.value
}
/** Whether we want to wait to show the bouncer in case passive auth succeeds. */
private fun usePrimaryBouncerPassiveAuthDelay(): Boolean {
val canRunFaceAuth = keyguardStateController.isFaceAuthEnabled &&
keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE)
val canRunActiveUnlock = trustRepository.isCurrentUserActiveUnlockAvailable.value &&
keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()
return featureFlags.isEnabled(Flags.DELAY_BOUNCER) &&
!needsFullscreenBouncer() &&
(canRunFaceAuth || canRunActiveUnlock)
}
companion object {
private const val TAG = "PrimaryBouncerInteractor"
}

View File

@@ -21,22 +21,23 @@ import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.RoboPilotTest
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepositoryImpl
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.RoboPilotTest
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.FakeSystemClock
import com.android.systemui.util.time.SystemClock
@@ -94,7 +95,8 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle
mock(DismissCallbackRegistry::class.java),
context,
mKeyguardUpdateMonitor,
mock(KeyguardBypassController::class.java),
mock(TrustRepository::class.java),
FakeFeatureFlags(),
)
mAlternateBouncerInteractor =
AlternateBouncerInteractor(

View File

@@ -35,12 +35,12 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepo
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -100,7 +100,8 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
mock(DismissCallbackRegistry::class.java),
context,
keyguardUpdateMonitor,
mock(KeyguardBypassController::class.java),
mock(TrustRepository::class.java),
FakeFeatureFlags(),
),
AlternateBouncerInteractor(
mock(StatusBarStateController::class.java),

View File

@@ -16,7 +16,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.os.Looper
import android.hardware.biometrics.BiometricSourceType
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.testing.TestableResources
@@ -28,15 +28,17 @@ import com.android.systemui.DejankUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.BouncerViewDelegate
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
@@ -67,16 +69,23 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
@Mock private lateinit var mPrimaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
@Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
@Mock private lateinit var keyguardBypassController: KeyguardBypassController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
private val mainHandler = FakeHandler(Looper.getMainLooper())
private lateinit var mainHandler: FakeHandler
private lateinit var underTest: PrimaryBouncerInteractor
private lateinit var resources: TestableResources
private lateinit var trustRepository: FakeTrustRepository
private lateinit var featureFlags: FakeFeatureFlags
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
.thenReturn(KeyguardSecurityModel.SecurityMode.PIN)
DejankUtils.setImmediate(true)
mainHandler = FakeHandler(android.os.Looper.getMainLooper())
trustRepository = FakeTrustRepository()
featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, false) }
underTest =
PrimaryBouncerInteractor(
repository,
@@ -89,7 +98,8 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
dismissCallbackRegistry,
context,
keyguardUpdateMonitor,
keyguardBypassController,
trustRepository,
featureFlags,
)
whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
whenever(repository.primaryBouncerShow.value).thenReturn(false)
@@ -383,6 +393,55 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() {
verify(repository).setSideFpsShowing(false)
}
@Test
fun delayBouncerWhenFaceAuthPossible() {
mainHandler.setMode(FakeHandler.Mode.QUEUEING)
// GIVEN bouncer should be delayed due to face auth
featureFlags.apply { set(Flags.DELAY_BOUNCER, true) }
whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
.thenReturn(true)
// WHEN bouncer show is requested
underTest.show(true)
// THEN primary show & primary showing soon aren't updated immediately
verify(repository, never()).setPrimaryShow(true)
verify(repository, never()).setPrimaryShowingSoon(false)
// WHEN all queued messages are dispatched
mainHandler.dispatchQueuedMessages()
// THEN primary show & primary showing soon are updated
verify(repository).setPrimaryShow(true)
verify(repository).setPrimaryShowingSoon(false)
}
@Test
fun delayBouncerWhenActiveUnlockPossible() {
mainHandler.setMode(FakeHandler.Mode.QUEUEING)
// GIVEN bouncer should be delayed due to active unlock
featureFlags.apply { set(Flags.DELAY_BOUNCER, true) }
trustRepository.setCurrentUserActiveUnlockAvailable(true)
whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState()).thenReturn(true)
// WHEN bouncer show is requested
underTest.show(true)
// THEN primary show & primary showing soon were scheduled to update
verify(repository, never()).setPrimaryShow(true)
verify(repository, never()).setPrimaryShowingSoon(false)
// WHEN all queued messages are dispatched
mainHandler.dispatchQueuedMessages()
// THEN primary show & primary showing soon are updated
verify(repository).setPrimaryShow(true)
verify(repository).setPrimaryShowingSoon(false)
}
private fun updateSideFpsVisibilityParameters(
isVisible: Boolean,
sfpsEnabled: Boolean,

View File

@@ -25,9 +25,11 @@ import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.utils.os.FakeHandler
@@ -37,6 +39,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@SmallTest
@@ -71,7 +74,8 @@ class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() {
dismissCallbackRegistry,
context,
keyguardUpdateMonitor,
keyguardBypassController,
Mockito.mock(TrustRepository::class.java),
FakeFeatureFlags(),
)
}

View File

@@ -24,9 +24,11 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.BouncerView
import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
@@ -42,6 +44,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@SmallTest
@@ -77,7 +80,8 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() {
dismissCallbackRegistry,
context,
keyguardUpdateMonitor,
keyguardBypassController,
Mockito.mock(TrustRepository::class.java),
FakeFeatureFlags(),
)
underTest = KeyguardBouncerViewModel(bouncerView, bouncerInteractor)
}

View File

@@ -19,13 +19,23 @@ package com.android.systemui.keyguard.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeTrustRepository : TrustRepository {
private val _isCurrentUserTrusted = MutableStateFlow(false)
override val isCurrentUserTrusted: Flow<Boolean>
get() = _isCurrentUserTrusted
private val _isCurrentUserActiveUnlockAvailable = MutableStateFlow(false)
override val isCurrentUserActiveUnlockAvailable: StateFlow<Boolean> =
_isCurrentUserActiveUnlockAvailable.asStateFlow()
fun setCurrentUserTrusted(trust: Boolean) {
_isCurrentUserTrusted.value = trust
}
fun setCurrentUserActiveUnlockAvailable(available: Boolean) {
_isCurrentUserActiveUnlockAvailable.value = available
}
}