Add repository to be the source of truth for device posture

Other changes:
 - Check config to see if face auth is supported by current posture

Bug: 262838215
Test: atest DevicePostureRepositoryTest
Change-Id: Idf5de785270b1c78e492039dfc03492b6ea5b7d6
This commit is contained in:
Chandru S
2023-02-16 09:30:29 -08:00
parent cc086f8d38
commit 2ff0225db8
8 changed files with 295 additions and 0 deletions

View File

@@ -24,8 +24,10 @@ import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback
import android.os.Looper
import android.os.UserHandle
import android.util.Log
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.AuthController
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -35,6 +37,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.user.data.repository.UserRepository
import java.io.PrintWriter
import javax.inject.Inject
@@ -47,8 +50,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -82,6 +87,12 @@ interface BiometricSettingsRepository {
/** Whether fingerprint feature is enabled for the current user by the DevicePolicy */
val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean>
/**
* Whether face authentication is supported for the current device posture. Face auth can be
* restricted to specific postures using [R.integer.config_face_auth_supported_posture]
*/
val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
}
@SysUISingleton
@@ -98,11 +109,27 @@ constructor(
@Background backgroundDispatcher: CoroutineDispatcher,
biometricManager: BiometricManager?,
@Main looper: Looper,
devicePostureRepository: DevicePostureRepository,
dumpManager: DumpManager,
) : BiometricSettingsRepository, Dumpable {
override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
init {
dumpManager.registerDumpable(this)
val configFaceAuthSupportedPosture =
DevicePosture.toPosture(
context.resources.getInteger(R.integer.config_face_auth_supported_posture)
)
isFaceAuthSupportedInCurrentPosture =
if (configFaceAuthSupportedPosture == DevicePosture.UNKNOWN) {
flowOf(true)
} else {
devicePostureRepository.currentDevicePosture.map {
it == configFaceAuthSupportedPosture
}
}
.onEach { Log.d(TAG, "isFaceAuthSupportedInCurrentPosture value changed to: $it") }
}
override fun dump(pw: PrintWriter, args: Array<String?>) {

View File

@@ -0,0 +1,58 @@
/*
* 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.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.statusbar.policy.DevicePostureController
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
/** Provide current device posture state. */
interface DevicePostureRepository {
/** Provides the current device posture. */
val currentDevicePosture: Flow<DevicePosture>
}
@SysUISingleton
class DevicePostureRepositoryImpl
@Inject
constructor(private val postureController: DevicePostureController) : DevicePostureRepository {
override val currentDevicePosture: Flow<DevicePosture>
get() = conflatedCallbackFlow {
val sendPostureUpdate = { posture: Int ->
val currentDevicePosture = DevicePosture.toPosture(posture)
trySendWithFailureLogging(
currentDevicePosture,
TAG,
"Error sending posture update to $currentDevicePosture"
)
}
val callback = DevicePostureController.Callback { sendPostureUpdate(it) }
postureController.addCallback(callback)
sendPostureUpdate(postureController.devicePosture)
awaitClose { postureController.removeCallback(callback) }
}
companion object {
const val TAG = "PostureRepositoryImpl"
}
}

View File

@@ -31,6 +31,8 @@ interface KeyguardRepositoryModule {
@Binds
fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository
@Binds fun devicePostureRepository(impl: DevicePostureRepositoryImpl): DevicePostureRepository
@Binds
fun biometricSettingsRepository(
impl: BiometricSettingsRepositoryImpl

View File

@@ -0,0 +1,41 @@
/*
* 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.systemui.statusbar.policy.DevicePostureController
/** Represents the possible posture states of the device. */
enum class DevicePosture {
UNKNOWN,
CLOSED,
HALF_OPENED,
OPENED,
FLIPPED;
companion object {
fun toPosture(@DevicePostureController.DevicePostureInt posture: Int): DevicePosture {
return when (posture) {
DevicePostureController.DEVICE_POSTURE_CLOSED -> CLOSED
DevicePostureController.DEVICE_POSTURE_HALF_OPENED -> HALF_OPENED
DevicePostureController.DEVICE_POSTURE_OPENED -> OPENED
DevicePostureController.DEVICE_POSTURE_FLIPPED -> FLIPPED
DevicePostureController.DEVICE_POSTURE_UNKNOWN -> UNKNOWN
else -> UNKNOWN
}
}
}
}

View File

@@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.coroutines.collectLastValue
@@ -38,11 +39,14 @@ import com.android.systemui.keyguard.data.repository.BiometricType.FACE
import com.android.systemui.keyguard.data.repository.BiometricType.REAR_FINGERPRINT
import com.android.systemui.keyguard.data.repository.BiometricType.SIDE_FINGERPRINT
import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT
import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -62,6 +66,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidTestingRunner::class)
@@ -78,6 +83,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
private lateinit var biometricManagerCallback:
ArgumentCaptor<IBiometricEnabledOnKeyguardCallback.Stub>
private lateinit var userRepository: FakeUserRepository
private lateinit var devicePostureRepository: FakeDevicePostureRepository
private lateinit var testDispatcher: TestDispatcher
private lateinit var testScope: TestScope
@@ -90,6 +96,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
testDispatcher = StandardTestDispatcher()
testScope = TestScope(testDispatcher)
userRepository = FakeUserRepository()
devicePostureRepository = FakeDevicePostureRepository()
}
private suspend fun createBiometricSettingsRepository() {
@@ -108,6 +115,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
looper = testableLooper!!.looper,
dumpManager = dumpManager,
biometricManager = biometricManager,
devicePostureRepository = devicePostureRepository,
)
testScope.runCurrent()
}
@@ -299,6 +307,50 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
verify(biometricManager, times(1)).registerEnabledOnKeyguardCallback(any())
}
@Test
fun faceAuthIsAlwaysSupportedIfSpecificPostureIsNotConfigured() =
testScope.runTest {
overrideResource(
R.integer.config_face_auth_supported_posture,
DevicePostureController.DEVICE_POSTURE_UNKNOWN
)
createBiometricSettingsRepository()
assertThat(collectLastValue(underTest.isFaceAuthSupportedInCurrentPosture)()).isTrue()
}
@Test
fun faceAuthIsSupportedOnlyWhenDevicePostureMatchesConfigValue() =
testScope.runTest {
overrideResource(
R.integer.config_face_auth_supported_posture,
DevicePostureController.DEVICE_POSTURE_FLIPPED
)
createBiometricSettingsRepository()
val isFaceAuthSupported =
collectLastValue(underTest.isFaceAuthSupportedInCurrentPosture)
assertThat(isFaceAuthSupported()).isFalse()
devicePostureRepository.setCurrentPosture(DevicePosture.CLOSED)
assertThat(isFaceAuthSupported()).isFalse()
devicePostureRepository.setCurrentPosture(DevicePosture.HALF_OPENED)
assertThat(isFaceAuthSupported()).isFalse()
devicePostureRepository.setCurrentPosture(DevicePosture.OPENED)
assertThat(isFaceAuthSupported()).isFalse()
devicePostureRepository.setCurrentPosture(DevicePosture.UNKNOWN)
assertThat(isFaceAuthSupported()).isFalse()
devicePostureRepository.setCurrentPosture(DevicePosture.FLIPPED)
assertThat(isFaceAuthSupported()).isTrue()
}
private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) {
authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled)
}

View File

@@ -0,0 +1,80 @@
/*
* 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.data.repository
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class DevicePostureRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DevicePostureRepository
private lateinit var testScope: TestScope
@Mock private lateinit var devicePostureController: DevicePostureController
@Captor private lateinit var callback: ArgumentCaptor<DevicePostureController.Callback>
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
testScope = TestScope()
underTest = DevicePostureRepositoryImpl(postureController = devicePostureController)
}
@Test
fun postureChangesArePropagated() =
testScope.runTest {
whenever(devicePostureController.devicePosture)
.thenReturn(DevicePostureController.DEVICE_POSTURE_FLIPPED)
val currentPosture = collectLastValue(underTest.currentDevicePosture)
assertThat(currentPosture()).isEqualTo(DevicePosture.FLIPPED)
verify(devicePostureController).addCallback(callback.capture())
callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_UNKNOWN)
assertThat(currentPosture()).isEqualTo(DevicePosture.UNKNOWN)
callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_CLOSED)
assertThat(currentPosture()).isEqualTo(DevicePosture.CLOSED)
callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED)
assertThat(currentPosture()).isEqualTo(DevicePosture.HALF_OPENED)
callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED)
assertThat(currentPosture()).isEqualTo(DevicePosture.OPENED)
callback.value.onPostureChanged(DevicePostureController.DEVICE_POSTURE_FLIPPED)
assertThat(currentPosture()).isEqualTo(DevicePosture.FLIPPED)
}
}

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf
class FakeBiometricSettingsRepository : BiometricSettingsRepository {
@@ -42,6 +43,9 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
override val isFingerprintEnabledByDevicePolicy =
_isFingerprintEnabledByDevicePolicy.asStateFlow()
override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean>
get() = flowOf(true)
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
}

View File

@@ -0,0 +1,31 @@
/*
* 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.data.repository
import com.android.systemui.keyguard.shared.model.DevicePosture
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeDevicePostureRepository : DevicePostureRepository {
private val _currentDevicePosture = MutableStateFlow(DevicePosture.UNKNOWN)
override val currentDevicePosture: Flow<DevicePosture>
get() = _currentDevicePosture
fun setCurrentPosture(posture: DevicePosture) {
_currentDevicePosture.value = posture
}
}