[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:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ class SceneTestUtils(
|
||||
repository = repository,
|
||||
backgroundDispatcher = testDispatcher,
|
||||
userRepository = userRepository,
|
||||
keyguardRepository = keyguardRepository,
|
||||
clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user