diff options
4 files changed, 184 insertions, 34 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index 0055f9a36529..0b6c7c415599 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -25,7 +25,6 @@ import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback import android.os.UserHandle import android.util.Log import com.android.internal.widget.LockPatternUtils -import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.AuthController @@ -36,13 +35,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.TAG +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.user.data.repository.UserRepository import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -108,10 +108,14 @@ interface BiometricSettingsRepository { * lockdown. */ val isCurrentUserInLockdown: Flow<Boolean> + + /** Authentication flags set for the current user. */ + val authenticationFlags: Flow<AuthenticationFlags> } -const val TAG = "BiometricsRepositoryImpl" +private const val TAG = "BiometricsRepositoryImpl" +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class BiometricSettingsRepositoryImpl @Inject @@ -129,6 +133,8 @@ constructor( dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { + private val biometricsEnabledForUser = mutableMapOf<Int, Boolean>() + override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> private val strongAuthTracker = StrongAuthTracker(userRepository, context) @@ -136,6 +142,9 @@ constructor( override val isCurrentUserInLockdown: Flow<Boolean> = strongAuthTracker.currentUserAuthFlags.map { it.isInUserLockdown } + override val authenticationFlags: Flow<AuthenticationFlags> = + strongAuthTracker.currentUserAuthFlags + init { Log.d(TAG, "Registering StrongAuthTracker") lockPatternUtils.registerStrongAuthTracker(strongAuthTracker) @@ -231,9 +240,14 @@ constructor( } } + private val isFaceEnabledByBiometricsManagerForCurrentUser: Flow<Boolean> = + userRepository.selectedUserInfo.flatMapLatest { userInfo -> + isFaceEnabledByBiometricsManager.map { biometricsEnabledForUser[userInfo.id] ?: false } + } + override val isFaceAuthenticationEnabled: Flow<Boolean> get() = - combine(isFaceEnabledByBiometricsManager, isFaceEnabledByDevicePolicy) { + combine(isFaceEnabledByBiometricsManagerForCurrentUser, isFaceEnabledByDevicePolicy) { biometricsManagerSetting, devicePolicySetting -> biometricsManagerSetting && devicePolicySetting @@ -249,13 +263,13 @@ constructor( .flowOn(backgroundDispatcher) .distinctUntilChanged() - private val isFaceEnabledByBiometricsManager = + private val isFaceEnabledByBiometricsManager: Flow<Pair<Int, Boolean>> = conflatedCallbackFlow { val callback = object : IBiometricEnabledOnKeyguardCallback.Stub() { override fun onChanged(enabled: Boolean, userId: Int) { trySendWithFailureLogging( - enabled, + Pair(userId, enabled), TAG, "biometricsEnabled state changed" ) @@ -264,9 +278,10 @@ constructor( biometricManager?.registerEnabledOnKeyguardCallback(callback) awaitClose {} } + .onEach { biometricsEnabledForUser[it.first] = it.second } // This is because the callback is binder-based and we want to avoid multiple callbacks // being registered. - .stateIn(scope, SharingStarted.Eagerly, false) + .stateIn(scope, SharingStarted.Eagerly, Pair(0, false)) override val isStrongBiometricAllowed: StateFlow<Boolean> = strongAuthTracker.isStrongBiometricAllowed.stateIn( @@ -306,14 +321,13 @@ constructor( ) } +@OptIn(ExperimentalCoroutinesApi::class) private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) : LockPatternUtils.StrongAuthTracker(context) { // Backing field for onStrongAuthRequiredChanged - private val _strongAuthFlags = - MutableStateFlow( - StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)) - ) + private val _authFlags = + MutableStateFlow(AuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId))) // Backing field for onIsNonStrongBiometricAllowedChanged private val _nonStrongBiometricAllowed = @@ -321,17 +335,15 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont Pair(currentUserId, isNonStrongBiometricAllowedAfterIdleTimeout(currentUserId)) ) - val currentUserAuthFlags: Flow<StrongAuthenticationFlags> = + val currentUserAuthFlags: Flow<AuthenticationFlags> = userRepository.selectedUserInfo .map { it.id } .distinctUntilChanged() .flatMapLatest { userId -> - _strongAuthFlags - .filter { it.userId == userId } + _authFlags + .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) } .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") } - .onStart { - emit(StrongAuthenticationFlags(userId, getStrongAuthForUser(userId))) - } + .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) } } /** isStrongBiometricAllowed for the current user. */ @@ -356,7 +368,7 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont override fun onStrongAuthRequiredChanged(userId: Int) { val newFlags = getStrongAuthForUser(userId) - _strongAuthFlags.value = StrongAuthenticationFlags(userId, newFlags) + _authFlags.value = AuthenticationFlags(userId, newFlags) Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags") } @@ -375,11 +387,3 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean = private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and policy) == 0 - -private data class StrongAuthenticationFlags(val userId: Int, val flag: Int) { - val isInUserLockdown = containsFlag(flag, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) -} - -private fun containsFlag(haystack: Int, needle: Int): Boolean { - return haystack and needle != 0 -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt new file mode 100644 index 000000000000..0bbf67fb49e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt @@ -0,0 +1,70 @@ +/* + * 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.shared.model + +import com.android.internal.widget.LockPatternUtils + +/** Authentication flags corresponding to a user. */ +data class AuthenticationFlags(val userId: Int, val flag: Int) { + val isInUserLockdown = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + ) + + val isPrimaryAuthRequiredAfterReboot = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT) + + val isPrimaryAuthRequiredAfterTimeout = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + + val isPrimaryAuthRequiredAfterDpmLockdown = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW + ) + + val someAuthRequiredAfterUserRequest = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) + + val someAuthRequiredAfterTrustAgentExpired = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED + ) + + val primaryAuthRequiredAfterLockout = + containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) + + val primaryAuthRequiredForUnattendedUpdate = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE + ) + + /** Either Class 3 biometrics or primary auth can be used to unlock the device. */ + val strongerAuthRequiredAfterNonStrongBiometricsTimeout = + containsFlag( + flag, + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT + ) +} + +private fun containsFlag(haystack: Int, needle: Int): Boolean { + return haystack and needle != 0 +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 1bab8170ccde..b925aeb10262 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -310,19 +310,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { createBiometricSettingsRepository() verify(biometricManager) .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) - whenever(devicePolicyManager.getKeyguardDisabledFeatures(isNull(), eq(PRIMARY_USER_ID))) .thenReturn(0) broadcastDPMStateChange() + val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + + assertThat(isFaceAuthEnabled()).isFalse() + + // Value changes for another user + biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) + assertThat(isFaceAuthEnabled()).isFalse() + + // Value changes for current user. biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID) - val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) assertThat(isFaceAuthEnabled()).isTrue() + } + + @Test + fun userChange_biometricEnabledChange_handlesRaceCondition() = + testScope.runTest { + createBiometricSettingsRepository() + verify(biometricManager) + .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture()) + val isFaceAuthEnabled = collectLastValue(underTest.isFaceAuthenticationEnabled) + biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID) + runCurrent() - biometricManagerCallback.value.onChanged(false, PRIMARY_USER_ID) + userRepository.setSelectedUserInfo(ANOTHER_USER) + runCurrent() - assertThat(isFaceAuthEnabled()).isFalse() + assertThat(isFaceAuthEnabled()).isTrue() } @Test @@ -382,7 +401,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { } @Test - fun userInLockdownUsesStrongAuthFlagsToDetermineValue() = + fun userInLockdownUsesAuthFlagsToDetermineValue() = testScope.runTest { createBiometricSettingsRepository() @@ -405,6 +424,38 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(isUserInLockdown()).isTrue() } + @Test + fun authFlagChangesForCurrentUserArePropagated() = + testScope.runTest { + createBiometricSettingsRepository() + + val authFlags = collectLastValue(underTest.authenticationFlags) + // has default value. + val defaultStrongAuthValue = STRONG_AUTH_REQUIRED_AFTER_BOOT + assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue) + + // change strong auth flags for another user. + // Combine with one more flag to check if we do the bitwise and + val inLockdown = + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN or STRONG_AUTH_REQUIRED_AFTER_TIMEOUT + onStrongAuthChanged(inLockdown, ANOTHER_USER_ID) + + // Still false. + assertThat(authFlags()!!.flag).isEqualTo(defaultStrongAuthValue) + + // change strong auth flags for current user. + onStrongAuthChanged(inLockdown, PRIMARY_USER_ID) + + assertThat(authFlags()!!.flag).isEqualTo(inLockdown) + + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, ANOTHER_USER_ID) + + assertThat(authFlags()!!.flag).isEqualTo(inLockdown) + + userRepository.setSelectedUserInfo(ANOTHER_USER) + assertThat(authFlags()!!.flag).isEqualTo(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) + } + private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) { authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index 65735f028c41..4aaf3478a31d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -17,10 +17,13 @@ package com.android.systemui.keyguard.data.repository +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.keyguard.shared.model.AuthenticationFlags import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map class FakeBiometricSettingsRepository : BiometricSettingsRepository { @@ -50,9 +53,12 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> get() = _isFaceAuthSupportedInCurrentPosture - private val _isCurrentUserInLockdown = MutableStateFlow(false) override val isCurrentUserInLockdown: Flow<Boolean> - get() = _isCurrentUserInLockdown + get() = _authFlags.map { it.isInUserLockdown } + + private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0)) + override val authenticationFlags: Flow<AuthenticationFlags> + get() = _authFlags fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { _isFingerprintEnrolled.value = isFingerprintEnrolled @@ -66,6 +72,10 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy } + fun setAuthenticationFlags(value: AuthenticationFlags) { + _authFlags.value = value + } + fun setFaceEnrolled(isFaceEnrolled: Boolean) { _isFaceEnrolled.value = isFaceEnrolled } @@ -79,7 +89,22 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { } fun setIsUserInLockdown(value: Boolean) { - _isCurrentUserInLockdown.value = value + if (value) { + setAuthenticationFlags( + AuthenticationFlags( + _authFlags.value.userId, + _authFlags.value.flag or + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + ) + ) + } else { + setAuthenticationFlags( + AuthenticationFlags( + _authFlags.value.userId, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED + ) + ) + } } fun setIsNonStrongBiometricAllowed(value: Boolean) { |