[flexiglass] Implement lockscreen bypass.

When bypass is enabled, the lock screen will be automatically dismissed
once the authentication challenge is completed. For example, completing
a biometric authentication challenge via face unlock or fingerprint
sensor can automatically bypass the lock screen.

It is deliberately not implemented as a Flow, since its state only needs
to be checked on demand.

Note: to enable this feature, the "Face auth modern arch" flag needs to
be enabled via Flag Flipper.

Fix: 290771600
Fix: 290404894
Test: new unit tests added
Test: manually verified in system UI that the lockscreen is skipped when
bypass is enabled in settings, and that it is not skipped when bypass is
disabled. To reach this setting, go to Settings > Security & privacy >
Device unlock > Face & Fingerprint unlock > Face Unlock > Skip lock
screen.

Change-Id: I393b9bcb1fed1299efaf8c426d269b4b83b36476
This commit is contained in:
burakov
2023-07-13 14:48:00 +00:00
parent 0e61b8a103
commit aec54aadc3
11 changed files with 65 additions and 81 deletions

View File

@@ -63,14 +63,6 @@ interface AuthenticationRepository {
*/
val isUnlocked: StateFlow<Boolean>
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismisses once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
val isBypassEnabled: StateFlow<Boolean>
/**
* Whether the auto confirm feature is enabled for the currently-selected user.
*
@@ -113,9 +105,6 @@ interface AuthenticationRepository {
*/
suspend fun isLockscreenEnabled(): Boolean
/** See [isBypassEnabled]. */
fun setBypassEnabled(isBypassEnabled: Boolean)
/** Reports an authentication attempt. */
suspend fun reportAuthenticationAttempt(isSuccessful: Boolean)
@@ -157,7 +146,7 @@ constructor(
private val lockPatternUtils: LockPatternUtils,
) : AuthenticationRepository {
override val isUnlocked: StateFlow<Boolean> = keyguardRepository.isKeyguardUnlocked
override val isUnlocked = keyguardRepository.isKeyguardUnlocked
override suspend fun isLockscreenEnabled(): Boolean {
return withContext(backgroundDispatcher) {
@@ -166,9 +155,6 @@ constructor(
}
}
private val _isBypassEnabled = MutableStateFlow(false)
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
override val isAutoConfirmEnabled: StateFlow<Boolean> =
userRepository.selectedUserInfo
.map { it.id }
@@ -225,10 +211,6 @@ constructor(
}
}
override fun setBypassEnabled(isBypassEnabled: Boolean) {
_isBypassEnabled.value = isBypassEnabled
}
override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
val selectedUserId = userRepository.selectedUserId
withContext(backgroundDispatcher) {

View File

@@ -24,6 +24,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationThrottling
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -51,6 +52,7 @@ constructor(
private val repository: AuthenticationRepository,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val userRepository: UserRepository,
private val keyguardRepository: KeyguardRepository,
private val clock: SystemClock,
) {
/**
@@ -76,14 +78,6 @@ constructor(
initialValue = true,
)
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismisses once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
/** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling
@@ -155,6 +149,16 @@ constructor(
return !isUnlocked.value && getAuthenticationMethod().isSecure
}
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismisses once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
fun isBypassEnabled(): Boolean {
return keyguardRepository.isBypassEnabled()
}
/**
* Attempts to authenticate the user and unlock the device.
*
@@ -218,11 +222,6 @@ constructor(
return authenticationResult.isSuccessful
}
/** See [isBypassEnabled]. */
fun toggleBypassEnabled() {
repository.setBypassEnabled(!repository.isBypassEnabled.value)
}
/** Starts refreshing the throttling state every second. */
private suspend fun startThrottlingCountdown() {
cancelCountdown()

View File

@@ -42,6 +42,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -171,6 +172,14 @@ interface KeyguardRepository {
*/
fun isKeyguardShowing(): Boolean
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismissed once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
fun isBypassEnabled(): Boolean
/** Sets whether the bottom area UI should animate the transition out of doze state. */
fun setAnimateDozingTransitions(animate: Boolean)
@@ -206,6 +215,7 @@ constructor(
wakefulnessLifecycle: WakefulnessLifecycle,
biometricUnlockController: BiometricUnlockController,
private val keyguardStateController: KeyguardStateController,
private val keyguardBypassController: KeyguardBypassController,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dozeTransitionListener: DozeTransitionListener,
private val dozeParameters: DozeParameters,
@@ -252,23 +262,17 @@ constructor(
override val isAodAvailable: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
object : DozeParameters.Callback {
override fun onAlwaysOnChange() {
trySendWithFailureLogging(
dozeParameters.getAlwaysOn(),
TAG,
"updated isAodAvailable"
)
}
DozeParameters.Callback {
trySendWithFailureLogging(
dozeParameters.alwaysOn,
TAG,
"updated isAodAvailable"
)
}
dozeParameters.addCallback(callback)
// Adding the callback does not send an initial update.
trySendWithFailureLogging(
dozeParameters.getAlwaysOn(),
TAG,
"initial isAodAvailable"
)
trySendWithFailureLogging(dozeParameters.alwaysOn, TAG, "initial isAodAvailable")
awaitClose { dozeParameters.removeCallback(callback) }
}
@@ -464,6 +468,10 @@ constructor(
return keyguardStateController.isShowing
}
override fun isBypassEnabled(): Boolean {
return keyguardBypassController.bypassEnabled
}
override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
val callback =
object : StatusBarStateController.StateListener {

View File

@@ -77,7 +77,7 @@ constructor(
authenticationInteractor.isUnlocked
.map { isUnlocked ->
val currentSceneKey = sceneInteractor.currentScene(CONTAINER_NAME).value.key
val isBypassEnabled = authenticationInteractor.isBypassEnabled.value
val isBypassEnabled = authenticationInteractor.isBypassEnabled()
when {
isUnlocked ->
when (currentSceneKey) {

View File

@@ -50,12 +50,6 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr
@DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN
private var pendingUnlock: PendingUnlock? = null
private val listeners = mutableListOf<OnBypassStateChangedListener>()
private val postureCallback = DevicePostureController.Callback { posture ->
if (postureState != posture) {
postureState = posture
notifyListeners()
}
}
private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback {
override fun onFaceAuthEnabledChanged() = notifyListeners()
}
@@ -162,10 +156,8 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr
val dismissByDefault = if (context.resources.getBoolean(
com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0
tunerService.addTunable(object : TunerService.Tunable {
override fun onTuningChanged(key: String?, newValue: String?) {
bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0
}
tunerService.addTunable({ key, _ ->
bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0
}, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD)
lockscreenUserManager.addUserChangedListener(
object : NotificationLockscreenUserManager.UserChangedListener {
@@ -281,8 +273,6 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr
}
companion object {
const val BYPASS_FADE_DURATION = 67
private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0
private const val FACE_UNLOCK_BYPASS_ALWAYS = 1
private const val FACE_UNLOCK_BYPASS_NEVER = 2

View File

@@ -102,19 +102,6 @@ class AuthenticationInteractorTest : SysuiTestCase() {
assertThat(isUnlocked).isFalse()
}
@Test
fun toggleBypassEnabled() =
testScope.runTest {
val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
assertThat(isBypassEnabled).isFalse()
underTest.toggleBypassEnabled()
assertThat(isBypassEnabled).isTrue()
underTest.toggleBypassEnabled()
assertThat(isBypassEnabled).isFalse()
}
@Test
fun isAuthenticationRequired_lockedAndSecured_true() =
testScope.runTest {

View File

@@ -41,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.whenever
@@ -73,6 +74,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var dozeTransitionListener: DozeTransitionListener
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardBypassController: KeyguardBypassController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
@Mock private lateinit var dozeParameters: DozeParameters
@@ -92,6 +94,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
wakefulnessLifecycle,
biometricUnlockController,
keyguardStateController,
keyguardBypassController,
keyguardUpdateMonitor,
dozeTransitionListener,
dozeParameters,
@@ -185,6 +188,20 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
job.cancel()
}
@Test
fun isBypassEnabled_disabledInController() {
whenever(keyguardBypassController.isBypassEnabled).thenReturn(false)
whenever(keyguardBypassController.bypassEnabled).thenReturn(false)
assertThat(underTest.isBypassEnabled()).isFalse()
}
@Test
fun isBypassEnabled_enabledInController() {
whenever(keyguardBypassController.isBypassEnabled).thenReturn(true)
whenever(keyguardBypassController.bypassEnabled).thenReturn(true)
assertThat(underTest.isBypassEnabled()).isTrue()
}
@Test
fun isAodAvailable() = runTest {
val flow = underTest.isAodAvailable

View File

@@ -28,7 +28,6 @@ import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -36,7 +35,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
@@ -385,7 +383,7 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
) {
featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled)
authenticationRepository.setUnlocked(isDeviceUnlocked)
authenticationRepository.setBypassEnabled(isBypassEnabled)
keyguardRepository.setBypassEnabled(isBypassEnabled)
initialSceneKey?.let {
sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it))
}

View File

@@ -31,9 +31,6 @@ class FakeAuthenticationRepository(
private val currentTime: () -> Long,
) : AuthenticationRepository {
private val _isBypassEnabled = MutableStateFlow(false)
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
private val _isAutoConfirmEnabled = MutableStateFlow(false)
override val isAutoConfirmEnabled: StateFlow<Boolean> = _isAutoConfirmEnabled.asStateFlow()
@@ -85,10 +82,6 @@ class FakeAuthenticationRepository(
return (credentialOverride ?: DEFAULT_PIN).size
}
override fun setBypassEnabled(isBypassEnabled: Boolean) {
_isBypassEnabled.value = isBypassEnabled
}
override suspend fun getFailedAuthenticationAttemptCount(): Int {
return failedAttemptCount
}

View File

@@ -114,6 +114,11 @@ class FakeKeyguardRepository : KeyguardRepository {
return _isKeyguardShowing.value
}
private var _isBypassEnabled = false
override fun isBypassEnabled(): Boolean {
return _isBypassEnabled
}
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
@@ -198,6 +203,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_isKeyguardUnlocked.value = isUnlocked
}
fun setBypassEnabled(isEnabled: Boolean) {
_isBypassEnabled = isEnabled
}
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}

View File

@@ -142,6 +142,7 @@ class SceneTestUtils(
repository = repository,
backgroundDispatcher = testDispatcher,
userRepository = userRepository,
keyguardRepository = keyguardRepository,
clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
)
}