From 882e1c36214616e03cdcf44d27c3437eec2ef3ac Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 7 Aug 2024 23:52:11 +0000 Subject: [PATCH 1/9] Split up FingerprintManagerInteractor Test: atest, screenshot tests passed Flag: com.android.settings.flags.fingerprint_v2_enrollment Change-Id: I70833d5d9888f730233a9757589ce7faa45eccc9 --- .../android/settings/SettingsApplication.java | 20 +- .../fingerprint2/BiometricsEnvironment.kt | 87 +++++---- .../repository/FingerprintEnrollmentRepo.kt | 98 ++++++++++ .../repository/FingerprintSensorRepository.kt | 12 +- .../FingerprintSettingsRepository.kt | 32 ++++ .../SimulatedTouchEventsRepository.kt | 1 - .../fingerprint2/data/repository/UserRepo.kt | 34 ++++ .../repository/UdfpsEnrollDebugRepository.kt | 2 + .../DebugTouchEventInteractorImpl.kt | 2 +- .../interactor/AuthenticateInteractorImpl.kt | 71 +++++++ .../CanEnrollFingerprintsInteractorImpl.kt | 31 ++++ .../EnrollFingerprintInteractorImpl.kt | 146 +++++++++++++++ .../EnrolledFingerprintsInteractorImpl.kt | 37 ++++ .../interactor/FingerprintEnrollInteractor.kt | 5 +- .../FingerprintManagerInteractorImpl.kt | 173 ------------------ .../interactor/FingerprintSensorInteractor.kt | 4 +- .../GenerateChallengeInteractorImpl.kt | 48 +++++ .../RemoveFingerprintsInteractorImpl.kt | 55 ++++++ .../RenameFingerprintsInteractorImpl.kt | 34 ++++ .../domain/interactor/SensorInteractorImpl.kt | 27 +++ .../domain/interactor/TouchEventInteractor.kt | 1 - .../fingerprint2/lib/AndroidManifest.xml | 2 +- .../interactor/AuthenitcateInteractor.kt | 25 +++ .../CanEnrollFingerprintsInteractor.kt | 27 +++ .../interactor/EnrollFingerprintInteractor.kt | 35 ++++ .../EnrolledFingerprintsInteractor.kt | 26 +++ .../FingerprintManagerInteractor.kt | 79 -------- .../interactor/GenerateChallengeInteractor.kt | 29 +++ .../interactor/RemoveFingerprintInteractor.kt | 28 +++ .../interactor/RenameFingerprintInteractor.kt | 25 +++ .../lib/domain/interactor/SensorInteractor.kt | 28 +++ .../FingerprintEnrollmentV2Activity.kt | 4 +- .../rfps/ui/viewmodel/RFPSViewModel.kt | 14 +- .../udfps/ui/viewmodel/UdfpsViewModel.kt | 17 +- .../FingerprintEnrollConfirmationViewModel.kt | 10 +- .../FingerprintEnrollFindSensorViewModel.kt | 24 +-- .../FingerprintEnrollIntroViewModel.kt | 12 +- .../viewmodel/FingerprintEnrollViewModel.kt | 17 +- .../FingerprintGatekeeperViewModel.kt | 8 +- .../FingerprintNavigationViewModel.kt | 8 +- .../fragment/FingerprintSettingsV2Fragment.kt | 35 ++-- .../FingerprintSettingsNavigationViewModel.kt | 18 +- .../viewmodel/FingerprintSettingsViewModel.kt | 57 ++++-- .../biometrics/fingerprint/Injector.kt | 3 +- .../FakeFingerprintManagerInteractor.kt | 33 +++- .../FingerprintManagerInteractorTest.kt | 160 ++++++++++++---- ...gerprintEnrollFindSensorViewModelV2Test.kt | 3 +- .../viewmodel/RFPSIconTouchViewModelTest.kt | 3 +- ...FingerprintEnrollEnrollingViewModelTest.kt | 3 +- ...gerprintSettingsNavigationViewModelTest.kt | 12 +- .../FingerprintSettingsViewModelTest.kt | 89 ++++----- 51 files changed, 1231 insertions(+), 523 deletions(-) create mode 100644 src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSettingsRepository.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt delete mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/SensorInteractorImpl.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/AuthenitcateInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrollFingerprintInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrolledFingerprintsInteractor.kt delete mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/GenerateChallengeInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RemoveFingerprintInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RenameFingerprintInteractor.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/SensorInteractor.kt diff --git a/src/com/android/settings/SettingsApplication.java b/src/com/android/settings/SettingsApplication.java index 5b052f22dc7..d208fdf09e1 100644 --- a/src/com/android/settings/SettingsApplication.java +++ b/src/com/android/settings/SettingsApplication.java @@ -18,7 +18,9 @@ package com.android.settings; import android.app.Application; import android.content.Context; +import android.content.pm.PackageManager; import android.database.ContentObserver; +import android.hardware.fingerprint.FingerprintManager; import android.net.Uri; import android.provider.Settings; import android.util.FeatureFlagUtils; @@ -74,9 +76,6 @@ public class SettingsApplication extends Application { // Set Spa environment. setSpaEnvironment(); - if (Flags.fingerprintV2Enrollment()) { - mBiometricsEnvironment = new BiometricsEnvironment(this); - } if (ActivityEmbeddingUtils.isSettingsSplitEnabled(this) && FeatureFlagUtils.isEnabled(this, @@ -120,7 +119,20 @@ public class SettingsApplication extends Application { @Nullable public BiometricsEnvironment getBiometricEnvironment() { - return mBiometricsEnvironment; + if (Flags.fingerprintV2Enrollment()) { + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + final FingerprintManager fpm = getSystemService(FingerprintManager.class); + if (mBiometricsEnvironment == null) { + mBiometricsEnvironment = new BiometricsEnvironment(this, fpm); + } + return mBiometricsEnvironment; + + } else { + return null; + } + + } + return null; } @Override diff --git a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt index 9bc920a96ba..e3233ed22b1 100644 --- a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt @@ -16,12 +16,9 @@ package com.android.settings.biometrics.fingerprint2 -import android.content.pm.PackageManager import android.hardware.fingerprint.FingerprintManager -import android.os.ServiceManager.ServiceNotFoundException import android.view.MotionEvent import android.view.accessibility.AccessibilityManager -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import com.android.internal.widget.LockPatternUtils @@ -29,33 +26,47 @@ import com.android.settings.SettingsApplication import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepository import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepositoryImpl +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepositoryImpl import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSettingsRepositoryImpl +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepoImpl import com.android.settings.biometrics.fingerprint2.debug.data.repository.UdfpsEnrollDebugRepositoryImpl import com.android.settings.biometrics.fingerprint2.debug.domain.interactor.DebugTouchEventInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.AuthenticateInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.CanEnrollFingerprintsInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollFingerprintInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractorImpl -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractor -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrolledFingerprintsInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintSensorInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintSensorInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.GenerateChallengeInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.RemoveFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.RenameFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.SensorInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.TouchEventInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.AuthenitcateInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.Settings import java.util.concurrent.Executors import kotlinx.coroutines.MainScope @@ -70,43 +81,53 @@ import kotlinx.coroutines.flow.flowOf * This code is instantiated within the [SettingsApplication], all repos should be private & * immutable and all interactors should public and immutable */ -class BiometricsEnvironment(context: SettingsApplication) : ViewModelStoreOwner { - +class BiometricsEnvironment( + val context: SettingsApplication, + private val fingerprintManager: FingerprintManager, +) : ViewModelStoreOwner { private val executorService = Executors.newSingleThreadExecutor() private val backgroundDispatcher = executorService.asCoroutineDispatcher() private val applicationScope = MainScope() private val gateKeeperPasswordProvider = GatekeeperPasswordProvider(LockPatternUtils(context)) - private val fingerprintManager = try { - if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - context.getSystemService(FragmentActivity.FINGERPRINT_SERVICE) as FingerprintManager? - } else { - null - } - } catch (exception: ServiceNotFoundException){ - null - } + private val userRepo = UserRepoImpl(context.userId) + private val fingerprintSettingsRepository = + FingerprintSettingsRepositoryImpl( + context.resources.getInteger( + com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser + ) + ) + private val fingerprintEnrollmentRepository = + FingerprintEnrollmentRepositoryImpl(fingerprintManager, userRepo, fingerprintSettingsRepository, + backgroundDispatcher, applicationScope) private val fingerprintSensorRepository: FingerprintSensorRepository = FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, applicationScope) private val debuggingRepository: DebuggingRepository = DebuggingRepositoryImpl() private val udfpsDebugRepo = UdfpsEnrollDebugRepositoryImpl() - /** For now, interactors are public to those with access to the [BiometricsEnvironment] class */ - val fingerprintEnrollInteractor: FingerprintEnrollInteractor by lazy { - FingerprintEnrollInteractorImpl(context, fingerprintManager, Settings) - } + fun createSensorPropertiesInteractor(): SensorInteractor = + SensorInteractorImpl(fingerprintSensorRepository) - /** [FingerprintManagerInteractor] to be used to construct view models */ - val fingerprintManagerInteractor: FingerprintManagerInteractor by lazy { - FingerprintManagerInteractorImpl( - context, - backgroundDispatcher, - fingerprintManager, - fingerprintSensorRepository, - gateKeeperPasswordProvider, - fingerprintEnrollInteractor, - ) - } + fun createCanEnrollFingerprintsInteractor(): CanEnrollFingerprintsInteractor = + CanEnrollFingerprintsInteractorImpl(fingerprintEnrollmentRepository) + + fun createGenerateChallengeInteractor(): GenerateChallengeInteractor = + GenerateChallengeInteractorImpl(fingerprintManager, context.userId, gateKeeperPasswordProvider) + + fun createFingerprintEnrollInteractor(): EnrollFingerprintInteractor = + EnrollFingerprintInteractorImpl(context.userId, fingerprintManager, Settings) + + fun createFingerprintsEnrolledInteractor(): EnrolledFingerprintsInteractorImpl = + EnrolledFingerprintsInteractorImpl(fingerprintManager, context.userId) + + fun createAuthenticateInteractor(): AuthenitcateInteractor = + AuthenticateInteractorImpl(fingerprintManager, context.userId) + + fun createRemoveFingerprintInteractor(): RemoveFingerprintInteractor = + RemoveFingerprintsInteractorImpl(fingerprintManager, context.userId) + + fun createRenameFingerprintInteractor(): RenameFingerprintInteractor = + RenameFingerprintsInteractorImpl(fingerprintManager, context.userId, backgroundDispatcher) val accessibilityInteractor: AccessibilityInteractor by lazy { AccessibilityInteractorImpl( diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt new file mode 100644 index 00000000000..22904e9d2ac --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.data.repository + +import android.hardware.biometrics.BiometricStateListener +import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** Repository that contains information about fingerprint enrollments. */ +interface FingerprintEnrollmentRepository { + /** The current enrollments of the user */ + val currentEnrollments: Flow?> + + /** Indicates if a user can enroll another fingerprint */ + val canEnrollUser: Flow + + fun maxFingerprintsEnrollable(): Int +} + +class FingerprintEnrollmentRepositoryImpl( + fingerprintManager: FingerprintManager, + userRepo: UserRepo, + private val settingsRepository: FingerprintSettingsRepository, + backgroundDispatcher: CoroutineDispatcher, + applicationScope: CoroutineScope, +) : FingerprintEnrollmentRepository { + + private val enrollmentChangedFlow: Flow = + callbackFlow { + val callback = + object : BiometricStateListener() { + override fun onEnrollmentsChanged(userId: Int, sensorId: Int, hasEnrollments: Boolean) { + trySend(userId) + } + } + withContext(backgroundDispatcher) { + fingerprintManager.registerBiometricStateListener(callback) + } + awaitClose { + // no way to unregister + } + } + .stateIn(applicationScope, started = SharingStarted.Eagerly, initialValue = null) + + override val currentEnrollments: Flow> = + userRepo.currentUser + .distinctUntilChanged() + .flatMapLatest { currentUser -> + enrollmentChangedFlow.map { enrollmentChanged -> + if (enrollmentChanged == null || enrollmentChanged == currentUser) { + fingerprintManager + .getEnrolledFingerprints(currentUser) + ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } + ?.toList() + } else { + null + } + } + } + .filterNotNull() + .flowOn(backgroundDispatcher) + + override val canEnrollUser: Flow = + currentEnrollments.map { + it?.size?.let { it < settingsRepository.maxEnrollableFingerprints() } ?: false + } + + override fun maxFingerprintsEnrollable(): Int { + return settingsRepository.maxEnrollableFingerprints() + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt index 516549e65f4..1cca532c6ac 100644 --- a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt @@ -31,6 +31,8 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transform import kotlinx.coroutines.withContext @@ -43,10 +45,13 @@ import kotlinx.coroutines.withContext interface FingerprintSensorRepository { /** Get the [FingerprintSensor] */ val fingerprintSensor: Flow + + /** Indicates if this device supports the side fingerprint sensor */ + val hasSideFps: Flow } class FingerprintSensorRepositoryImpl( - fingerprintManager: FingerprintManager?, + private val fingerprintManager: FingerprintManager, backgroundDispatcher: CoroutineDispatcher, activityScope: CoroutineScope, ) : FingerprintSensorRepository { @@ -66,7 +71,7 @@ class FingerprintSensorRepositoryImpl( } } withContext(backgroundDispatcher) { - fingerprintManager?.addAuthenticatorsRegisteredCallback(callback) + fingerprintManager.addAuthenticatorsRegisteredCallback(callback) } awaitClose {} } @@ -75,6 +80,9 @@ class FingerprintSensorRepositoryImpl( override val fingerprintSensor: Flow = fingerprintPropsInternal.transform { emit(it.toFingerprintSensor()) } + override val hasSideFps: Flow = + fingerprintSensor.flatMapLatest { flow { emit(fingerprintManager.isPowerbuttonFps()) } } + companion object { private val DEFAULT_PROPS = diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSettingsRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSettingsRepository.kt new file mode 100644 index 00000000000..fe6676c1608 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSettingsRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.data.repository + +/** + * Repository for storing metadata about fingerprint enrollments. + */ +interface FingerprintSettingsRepository { + /** + * Indicates the maximum number of fingerprints enrollable + */ + fun maxEnrollableFingerprints(): Int +} + +class FingerprintSettingsRepositoryImpl(private val maxFingerprintsEnrollable: Int) : + FingerprintSettingsRepository { + override fun maxEnrollableFingerprints() = maxFingerprintsEnrollable +} diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt index 3c355e76853..9b7f2808ee7 100644 --- a/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/SimulatedTouchEventsRepository.kt @@ -16,7 +16,6 @@ package com.android.settings.biometrics.fingerprint2.data.repository -import android.graphics.Point import android.view.MotionEvent import kotlinx.coroutines.flow.Flow diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt new file mode 100644 index 00000000000..720e7787d12 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * A repository responsible for indicating the current user. + */ +interface UserRepo { + /** + * This flow indicates the current user. + */ + val currentUser: Flow +} + +class UserRepoImpl(val currUser: Int): UserRepo { + override val currentUser: Flow = flowOf(currUser) +} diff --git a/src/com/android/settings/biometrics/fingerprint2/debug/data/repository/UdfpsEnrollDebugRepository.kt b/src/com/android/settings/biometrics/fingerprint2/debug/data/repository/UdfpsEnrollDebugRepository.kt index 0c3152a83ae..bc48f074329 100644 --- a/src/com/android/settings/biometrics/fingerprint2/debug/data/repository/UdfpsEnrollDebugRepository.kt +++ b/src/com/android/settings/biometrics/fingerprint2/debug/data/repository/UdfpsEnrollDebugRepository.kt @@ -97,6 +97,8 @@ class UdfpsEnrollDebugRepositoryImpl : } override val fingerprintSensor: Flow = flowOf(sensorProps) + override val hasSideFps: Flow + get() = flowOf(false) private fun pointToLeftOfSensor(sensorLocation: Rect): MotionEvent = MotionEvent.obtain( diff --git a/src/com/android/settings/biometrics/fingerprint2/debug/domain/interactor/DebugTouchEventInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/debug/domain/interactor/DebugTouchEventInteractorImpl.kt index fff6b6609fd..f6627e1d8e9 100644 --- a/src/com/android/settings/biometrics/fingerprint2/debug/domain/interactor/DebugTouchEventInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/debug/domain/interactor/DebugTouchEventInteractorImpl.kt @@ -26,4 +26,4 @@ class DebugTouchEventInteractorImpl( ) : TouchEventInteractor { override val touchEvent: Flow = udfpsSimulatedTouchEventsRepository.touchExplorationDebug -} \ No newline at end of file +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt new file mode 100644 index 00000000000..df93092619f --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/AuthenticateInteractorImpl.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.hardware.fingerprint.FingerprintManager +import android.os.CancellationSignal +import android.util.Log +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.AuthenitcateInteractor +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +class AuthenticateInteractorImpl( + private val fingerprintManager: FingerprintManager, + private val userId: Int, +) : AuthenitcateInteractor { + + override suspend fun authenticate(): FingerprintAuthAttemptModel = + suspendCancellableCoroutine { c: CancellableContinuation -> + val authenticationCallback = + object : FingerprintManager.AuthenticationCallback() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + if (c.isCompleted) { + Log.d(TAG, "framework sent down onAuthError after finish") + return + } + c.resume(FingerprintAuthAttemptModel.Error(errorCode, errString.toString())) + } + + override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + if (c.isCompleted) { + Log.d(TAG, "framework sent down onAuthError after finish") + return + } + c.resume(FingerprintAuthAttemptModel.Success(result.fingerprint?.biometricId ?: -1)) + } + } + + val cancellationSignal = CancellationSignal() + c.invokeOnCancellation { cancellationSignal.cancel() } + fingerprintManager.authenticate( + null, + cancellationSignal, + authenticationCallback, + null, + userId, + ) + } + + companion object { + private const val TAG = "AuthenticateInteractor" + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt new file mode 100644 index 00000000000..caeea4e4586 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepository +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor +import kotlinx.coroutines.flow.Flow + +class CanEnrollFingerprintsInteractorImpl( + val fingerprintEnrollmentRepository: FingerprintEnrollmentRepository +) : CanEnrollFingerprintsInteractor { + override val canEnrollFingerprints: Flow = fingerprintEnrollmentRepository.canEnrollUser + /** Indicates the maximum fingerprints enrollable for a given user */ + override fun maxFingerprintsEnrollable(): Int { + return fingerprintEnrollmentRepository.maxFingerprintsEnrollable() + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt new file mode 100644 index 00000000000..3e14a640278 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollFingerprintInteractorImpl.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.hardware.fingerprint.FingerprintEnrollOptions +import android.hardware.fingerprint.FingerprintManager +import android.os.CancellationSignal +import android.util.Log +import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError +import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason +import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow +import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.update + +class EnrollFingerprintInteractorImpl( + private val userId: Int, + private val fingerprintManager: FingerprintManager, + private val fingerprintFlow: FingerprintFlow, +) : EnrollFingerprintInteractor { + private val enrollRequestOutstanding = MutableStateFlow(false) + + override suspend fun enroll( + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason, + fingerprintEnrollOptions: FingerprintEnrollOptions, + ): Flow = callbackFlow { + // TODO (b/308456120) Improve this logic + if (enrollRequestOutstanding.value) { + Log.d(TAG, "Outstanding enroll request, waiting 150ms") + delay(150) + if (enrollRequestOutstanding.value) { + Log.e(TAG, "Request still present, continuing") + } + } + + enrollRequestOutstanding.update { true } + + var streamEnded = false + var totalSteps: Int? = null + val enrollmentCallback = + object : FingerprintManager.EnrollmentCallback() { + override fun onEnrollmentProgress(remaining: Int) { + // This is sort of an implementation detail, but unfortunately the API isn't + // very expressive. If anything we should look at changing the FingerprintManager API. + if (totalSteps == null) { + totalSteps = remaining + 1 + } + + trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { error -> + Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error") + } + + if (remaining == 0) { + streamEnded = true + enrollRequestOutstanding.update { false } + } + } + + override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) { + trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString())).onFailure { error + -> + Log.d(TAG, "onEnrollmentHelp failed to send, due to $error") + } + } + + override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) { + trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)).onFailure { error -> + Log.d(TAG, "onEnrollmentError failed to send, due to $error") + } + Log.d(TAG, "onEnrollmentError($errMsgId)") + streamEnded = true + enrollRequestOutstanding.update { false } + } + + override fun onUdfpsPointerDown(sensorId: Int) { + trySend(FingerEnrollState.PointerDown(sensorId)).onFailure { error -> + Log.d(TAG, "onUdfpsPointerDown failed to send, due to $error") + } + } + + override fun onUdfpsPointerUp(sensorId: Int) { + trySend(FingerEnrollState.PointerUp(sensorId)).onFailure { error -> + Log.d(TAG, "onUdfpsPointerUp failed to send, due to $error") + } + } + + override fun onUdfpsOverlayShown() { + trySend(FingerEnrollState.OverlayShown).onFailure { error -> + Log.d(TAG, "OverlayShown failed to send, due to $error") + } + } + + override fun onAcquired(isAcquiredGood: Boolean) { + trySend(FingerEnrollState.Acquired(isAcquiredGood)).onFailure { error -> + Log.d(TAG, "Acquired failed to send, due to $error") + } + } + } + + val cancellationSignal = CancellationSignal() + + fingerprintManager.enroll( + hardwareAuthToken, + cancellationSignal, + userId, + enrollmentCallback, + enrollReason.toOriginalReason(), + fingerprintEnrollOptions, + ) + awaitClose { + // If the stream has not been ended, and the user has stopped collecting the flow + // before it was over, send cancel. + if (!streamEnded) { + Log.e(TAG, "Cancel is sent from settings for enroll()") + cancellationSignal.cancel() + } + } + } + + companion object { + private const val TAG = "FingerprintEnrollStateRepository" + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt new file mode 100644 index 00000000000..83b532ecd98 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class EnrolledFingerprintsInteractorImpl( + private val fingerprintManager: FingerprintManager, + userId: Int, +) : EnrolledFingerprintsInteractor { + override val enrolledFingerprints: Flow?> = flow { + emit( + fingerprintManager + .getEnrolledFingerprints(userId) + ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } + ?.toList() + ) + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt index a36832db076..56a125794b7 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt @@ -16,7 +16,6 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor -import android.content.Context import android.hardware.fingerprint.FingerprintEnrollOptions import android.hardware.fingerprint.FingerprintManager import android.os.CancellationSignal @@ -49,7 +48,7 @@ interface FingerprintEnrollInteractor { } class FingerprintEnrollInteractorImpl( - private val applicationContext: Context, + private val userId: Int, private val fingerprintManager: FingerprintManager?, private val fingerprintFlow: FingerprintFlow, ) : FingerprintEnrollInteractor { @@ -138,7 +137,7 @@ class FingerprintEnrollInteractorImpl( fingerprintManager?.enroll( hardwareAuthToken, cancellationSignal, - applicationContext.userId, + userId, enrollmentCallback, enrollReason.toOriginalReason(), fingerprintEnrollOptions, diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt deleted file mode 100644 index f03c94ea047..00000000000 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.settings.biometrics.fingerprint2.domain.interactor - -import android.content.Context -import android.content.Intent -import android.hardware.fingerprint.FingerprintEnrollOptions -import android.hardware.fingerprint.FingerprintManager -import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback -import android.hardware.fingerprint.FingerprintManager.RemovalCallback -import android.os.CancellationSignal -import android.util.Log -import com.android.settings.biometrics.GatekeeperPasswordProvider -import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor -import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel -import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData -import com.android.settings.password.ChooseLockSettingsHelper -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext - -private const val TAG = "FingerprintManagerInteractor" - -class FingerprintManagerInteractorImpl( - applicationContext: Context, - private val backgroundDispatcher: CoroutineDispatcher, - private val fingerprintManager: FingerprintManager?, - fingerprintSensorRepository: FingerprintSensorRepository, - private val gatekeeperPasswordProvider: GatekeeperPasswordProvider, - private val fingerprintEnrollStateRepository: FingerprintEnrollInteractor, -) : FingerprintManagerInteractor { - - private val maxFingerprints = - applicationContext.resources.getInteger( - com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser - ) - private val applicationContext = applicationContext.applicationContext - - override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair = - suspendCoroutine { - val callback = GenerateChallengeCallback { _, userId, challenge -> - val intent = Intent() - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle) - val challengeToken = - gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId) - - gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false) - val p = Pair(challenge, challengeToken) - it.resume(p) - } - fingerprintManager?.generateChallenge(applicationContext.userId, callback) - } - - override val enrolledFingerprints: Flow?> = flow { - emit( - fingerprintManager?.getEnrolledFingerprints(applicationContext.userId) - ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }?.toList() - ) - } - - override val canEnrollFingerprints: Flow = flow { - emit( - fingerprintManager?.getEnrolledFingerprints(applicationContext.userId)?.size ?: maxFingerprints < maxFingerprints - ) - } - - override val sensorPropertiesInternal = fingerprintSensorRepository.fingerprintSensor - - override val maxEnrollableFingerprints = flow { emit(maxFingerprints) } - - override suspend fun enroll( - hardwareAuthToken: ByteArray?, - enrollReason: EnrollReason, - fingerprintEnrollOptions: FingerprintEnrollOptions, - ): Flow = - fingerprintEnrollStateRepository.enroll( - hardwareAuthToken, - enrollReason, - fingerprintEnrollOptions, - ) - - override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine { - val callback = - object : RemovalCallback() { - override fun onRemovalError( - fp: android.hardware.fingerprint.Fingerprint, - errMsgId: Int, - errString: CharSequence, - ) { - it.resume(false) - } - - override fun onRemovalSucceeded( - fp: android.hardware.fingerprint.Fingerprint?, - remaining: Int, - ) { - it.resume(true) - } - } - fingerprintManager?.remove( - android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId), - applicationContext.userId, - callback, - ) - } - - override suspend fun renameFingerprint(fp: FingerprintData, newName: String) { - withContext(backgroundDispatcher) { - fingerprintManager?.rename(fp.fingerId, applicationContext.userId, newName) - } - } - - override suspend fun hasSideFps(): Boolean? = suspendCancellableCoroutine { - it.resume(fingerprintManager?.isPowerbuttonFps) - } - - override suspend fun authenticate(): FingerprintAuthAttemptModel = - suspendCancellableCoroutine { c: CancellableContinuation -> - val authenticationCallback = - object : FingerprintManager.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - if (c.isCompleted) { - Log.d(TAG, "framework sent down onAuthError after finish") - return - } - c.resume(FingerprintAuthAttemptModel.Error(errorCode, errString.toString())) - } - - override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - if (c.isCompleted) { - Log.d(TAG, "framework sent down onAuthError after finish") - return - } - c.resume(FingerprintAuthAttemptModel.Success(result.fingerprint?.biometricId ?: -1)) - } - } - - val cancellationSignal = CancellationSignal() - c.invokeOnCancellation { cancellationSignal.cancel() } - fingerprintManager?.authenticate( - null, - cancellationSignal, - authenticationCallback, - null, - applicationContext.userId, - ) - } -} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintSensorInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintSensorInteractor.kt index 073629c6537..7b1d4fd280c 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintSensorInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintSensorInteractor.kt @@ -20,9 +20,7 @@ import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintS import com.android.systemui.biometrics.shared.model.FingerprintSensor import kotlinx.coroutines.flow.Flow -/** - * Interactor that propagates the type of [FingerprintSensor] this device supports. - */ +/** Interactor that propagates the type of [FingerprintSensor] this device supports. */ interface FingerprintSensorInteractor { /** Get the [FingerprintSensor] */ val fingerprintSensor: Flow diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt new file mode 100644 index 00000000000..a2080fbb74f --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/GenerateChallengeInteractorImpl.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.content.Intent +import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.GatekeeperPasswordProvider +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor +import com.android.settings.password.ChooseLockSettingsHelper +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class GenerateChallengeInteractorImpl( + private val fingerprintManager: FingerprintManager, + private val userId: Int, + private val gatekeeperPasswordProvider: GatekeeperPasswordProvider, +) : GenerateChallengeInteractor { + + override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair = + suspendCoroutine { + val callback = + FingerprintManager.GenerateChallengeCallback { _, userId, challenge -> + val intent = Intent() + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle) + val challengeToken = + gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId) + + gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false) + val p = Pair(challenge, challengeToken) + it.resume(p) + } + fingerprintManager.generateChallenge(userId, callback) + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt new file mode 100644 index 00000000000..4232963ad55 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RemoveFingerprintsInteractorImpl.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.hardware.fingerprint.FingerprintManager +import android.hardware.fingerprint.FingerprintManager.RemovalCallback +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class RemoveFingerprintsInteractorImpl( + private val fingerprintManager: FingerprintManager, + private val userId: Int, +) : RemoveFingerprintInteractor { + + override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine { + val callback = + object : RemovalCallback() { + override fun onRemovalError( + fp: android.hardware.fingerprint.Fingerprint, + errMsgId: Int, + errString: CharSequence, + ) { + it.resume(false) + } + + override fun onRemovalSucceeded( + fp: android.hardware.fingerprint.Fingerprint?, + remaining: Int, + ) { + it.resume(true) + } + } + fingerprintManager.remove( + android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId), + userId, + callback, + ) + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt new file mode 100644 index 00000000000..f238e7c4f2e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/RenameFingerprintsInteractorImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +class RenameFingerprintsInteractorImpl( + private val fingerprintManager: FingerprintManager, + private val userId: Int, + private val backgroundDispatcher: CoroutineDispatcher, +) : RenameFingerprintInteractor { + + override suspend fun renameFingerprint(fp: FingerprintData, newName: String) { + withContext(backgroundDispatcher) { fingerprintManager.rename(fp.fingerId, userId, newName) } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/SensorInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/SensorInteractorImpl.kt new file mode 100644 index 00000000000..7df0795e4ec --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/SensorInteractorImpl.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.domain.interactor + +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor +import kotlinx.coroutines.flow.Flow + +class SensorInteractorImpl(private val repo: FingerprintSensorRepository) : + SensorInteractor { + override val sensorPropertiesInternal = repo.fingerprintSensor + override val hasSideFps: Flow = repo.hasSideFps +} diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/TouchEventInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/TouchEventInteractor.kt index 4ef2afa0875..778837d84ea 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/TouchEventInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/TouchEventInteractor.kt @@ -24,4 +24,3 @@ interface TouchEventInteractor { /** A flow simulating user touches. */ val touchEvent: Flow } - diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/AndroidManifest.xml b/src/com/android/settings/biometrics/fingerprint2/lib/AndroidManifest.xml index 250f0af83eb..0b7ea2841ef 100644 --- a/src/com/android/settings/biometrics/fingerprint2/lib/AndroidManifest.xml +++ b/src/com/android/settings/biometrics/fingerprint2/lib/AndroidManifest.xml @@ -13,6 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/AuthenitcateInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/AuthenitcateInteractor.kt new file mode 100644 index 00000000000..4fc94137bc0 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/AuthenitcateInteractor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel + +/** Interactor responsible for coordinating authentication. */ +interface AuthenitcateInteractor { + /** Runs the authenticate flow */ + suspend fun authenticate(): FingerprintAuthAttemptModel +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt new file mode 100644 index 00000000000..11a9258ed88 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import kotlinx.coroutines.flow.Flow + +/** Returns whether or not a user can enroll a fingerprint */ +interface CanEnrollFingerprintsInteractor { + /** Returns true if a user can enroll a fingerprint false otherwise. */ + val canEnrollFingerprints: Flow + /** Indicates the maximum fingerprints enrollable for a given user */ + fun maxFingerprintsEnrollable(): Int +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrollFingerprintInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrollFingerprintInteractor.kt new file mode 100644 index 00000000000..be7b4d042ee --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrollFingerprintInteractor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import android.hardware.fingerprint.FingerprintEnrollOptions +import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason +import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState +import kotlinx.coroutines.flow.Flow + +/** Interactor that enrolls a fingerprint */ +interface EnrollFingerprintInteractor { + /** + * Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this + * enrollment. If successful data in the [fingerprintEnrollState] should be populated. + */ + suspend fun enroll( + hardwareAuthToken: ByteArray?, + enrollReason: EnrollReason, + fingerprintEnrollOptions: FingerprintEnrollOptions, + ): Flow +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrolledFingerprintsInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrolledFingerprintsInteractor.kt new file mode 100644 index 00000000000..14fc1e5e215 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/EnrolledFingerprintsInteractor.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import kotlinx.coroutines.flow.Flow + +/** Interface to obtain the enrolled fingerprints */ +interface EnrolledFingerprintsInteractor { + /** Returns the list of current fingerprints. */ + val enrolledFingerprints: Flow?> +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt deleted file mode 100644 index 5f4cecabab1..00000000000 --- a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor - -import android.hardware.fingerprint.FingerprintEnrollOptions -import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason -import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel -import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData -import com.android.systemui.biometrics.shared.model.FingerprintSensor -import kotlinx.coroutines.flow.Flow - -/** - * Interface to obtain the necessary data for FingerprintEnrollment/Settings - * - * Note that this interface should not have dependencies on heavyweight libraries such as the - * framework, hidl/aidl, etc. This makes it much easier to test and create fakes for. - */ -interface FingerprintManagerInteractor { - /** Returns the list of current fingerprints. */ - val enrolledFingerprints: Flow?> - - /** Returns the max enrollable fingerprints, note during SUW this might be 1 */ - val maxEnrollableFingerprints: Flow - - /** Returns true if a user can enroll a fingerprint false otherwise. */ - val canEnrollFingerprints: Flow - - /** Retrieves the sensor properties of a device */ - val sensorPropertiesInternal: Flow - - /** Runs the authenticate flow */ - suspend fun authenticate(): FingerprintAuthAttemptModel - - /** - * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a - * challenge and challenge token. This info can be used for secure operations such as enrollment - * - * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm - * @return A [Pair] of the challenge and challenge token - */ - suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair - - /** - * Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this - * enrollment. If successful data in the [fingerprintEnrollState] should be populated. - */ - suspend fun enroll( - hardwareAuthToken: ByteArray?, - enrollReason: EnrollReason, - fingerprintEnrollOptions: FingerprintEnrollOptions, - ): Flow - - /** - * Removes the given fingerprint, returning true if it was successfully removed and false - * otherwise - */ - suspend fun removeFingerprint(fp: FingerprintData): Boolean - - /** Renames the given fingerprint if one exists */ - suspend fun renameFingerprint(fp: FingerprintData, newName: String) - - /** Indicates if the device has side fingerprint */ - suspend fun hasSideFps(): Boolean? -} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/GenerateChallengeInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/GenerateChallengeInteractor.kt new file mode 100644 index 00000000000..82667fe0465 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/GenerateChallengeInteractor.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +/** This interactor is responsible for generating a challenge. */ +interface GenerateChallengeInteractor { + /** + * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a + * challenge and challenge token. This info can be used for secure operations such as enrollment + * + * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm + * @return A [Pair] of the challenge and challenge token + */ + suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RemoveFingerprintInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RemoveFingerprintInteractor.kt new file mode 100644 index 00000000000..6d0e5641079 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RemoveFingerprintInteractor.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData + +/** Interactor in charge of removing a fingerprint */ +interface RemoveFingerprintInteractor { + /** + * Removes the given fingerprint, returning true if it was successfully removed and false + * otherwise + */ + suspend fun removeFingerprint(fp: FingerprintData): Boolean +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RenameFingerprintInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RenameFingerprintInteractor.kt new file mode 100644 index 00000000000..d7fe1c0850f --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/RenameFingerprintInteractor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData + +/** Interactor that can rename a fingerprint. */ +interface RenameFingerprintInteractor { + /** Renames the given fingerprint if one exists */ + suspend fun renameFingerprint(fp: FingerprintData, newName: String) +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/SensorInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/SensorInteractor.kt new file mode 100644 index 00000000000..f265c327af3 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/SensorInteractor.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 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.settings.biometrics.fingerprint2.lib.domain.interactor + +import com.android.systemui.biometrics.shared.model.FingerprintSensor +import kotlinx.coroutines.flow.Flow + +/** Interactor that has various information about a fingerprint sensor */ +interface SensorInteractor { + /** Retrieves the sensor properties of the device */ + val sensorPropertiesInternal: Flow + /** Indicates if the device supports side fps */ + val hasSideFps: Flow +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index 421548fabf9..77d070e5026 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -96,8 +96,8 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { } /** - * View models below this line are not used by this class but must be initialized - * in the activity view model store to be used by other view models. + * View models below this line are not used by this class but must be initialized in the activity + * view model store to be used by other view models. */ private val fingerprintEnrollViewModel: FingerprintEnrollViewModel by viewModels { FingerprintEnrollViewModel.Factory diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt index c95020dff3c..932c40839af 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel @@ -46,10 +46,10 @@ import kotlinx.coroutines.flow.update /** View Model used by the rear fingerprint enrollment fragment. */ class RFPSViewModel( - private val fingerprintEnrollViewModel: FingerprintEnrollEnrollingViewModel, - private val navigationViewModel: FingerprintNavigationViewModel, - orientationInteractor: OrientationInteractor, - private val fingerprintManager: FingerprintManagerInteractor, + private val fingerprintEnrollViewModel: FingerprintEnrollEnrollingViewModel, + private val navigationViewModel: FingerprintNavigationViewModel, + orientationInteractor: OrientationInteractor, + private val sensorInteractor: SensorInteractor, ) : ViewModel() { private val _textViewIsVisible = MutableStateFlow(false) @@ -62,7 +62,7 @@ class RFPSViewModel( val shouldAnimateIcon = _shouldAnimateIcon private var enrollFlow: Flow = - fingerprintManager.sensorPropertiesInternal.filterNotNull().combine( + sensorInteractor.sensorPropertiesInternal.filterNotNull().combine( fingerprintEnrollViewModel.enrollFlow ) { props, enroll -> if (props.sensorType == FingerprintSensorType.REAR) { @@ -181,7 +181,7 @@ class RFPSViewModel( provider[FingerprintEnrollEnrollingViewModel::class], provider[FingerprintNavigationViewModel::class], biometricEnvironment.orientationInteractor, - biometricEnvironment.fingerprintManagerInteractor, + biometricEnvironment.createSensorPropertiesInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt index 3396cdc5243..658c6c747c2 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt @@ -38,7 +38,7 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Orientatio import com.android.settings.biometrics.fingerprint2.domain.interactor.TouchEventInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText @@ -76,17 +76,17 @@ class UdfpsViewModel( enrollStageInteractor: EnrollStageInteractor, orientationInteractor: OrientationInteractor, udfpsEnrollInteractor: UdfpsEnrollInteractor, - fingerprintManager: FingerprintManagerInteractor, accessibilityInteractor: AccessibilityInteractor, sensorRepository: FingerprintSensorInteractor, touchEventInteractor: TouchEventInteractor, + sensorInteractor: SensorInteractor, ) : ViewModel() { private val isSetupWizard = flowOf(false) private var shouldResetErollment = false private var _enrollState: Flow = - fingerprintManager.sensorPropertiesInternal.filterNotNull().combine( + sensorInteractor.sensorPropertiesInternal.filterNotNull().combine( fingerprintEnrollEnrollingViewModel.enrollFlow ) { props, enroll -> if (props.sensorType.isUdfps()) { @@ -198,8 +198,7 @@ class UdfpsViewModel( .distinctUntilChanged() private val _touchEvent: MutableStateFlow = MutableStateFlow(null) - val touchEvent = - _touchEvent.asStateFlow().filterNotNull() + val touchEvent = _touchEvent.asStateFlow().filterNotNull() /** Determines the current [EnrollStageModel] enrollment is in */ private val enrollStage: Flow = @@ -267,11 +266,7 @@ class UdfpsViewModel( backgroundViewModel.background.filter { it }.collect { didGoToBackground() } } - viewModelScope.launch { - touchEventInteractor.touchEvent.collect { - _touchEvent.update { it } - } - } + viewModelScope.launch { touchEventInteractor.touchEvent.collect { _touchEvent.update { it } } } } /** Indicates if we should show the lottie. */ @@ -430,10 +425,10 @@ class UdfpsViewModel( biometricEnvironment.enrollStageInteractor, biometricEnvironment.orientationInteractor, biometricEnvironment.udfpsEnrollInteractor, - biometricEnvironment.fingerprintManagerInteractor, biometricEnvironment.accessibilityInteractor, biometricEnvironment.sensorInteractor, biometricEnvironment.touchEventInteractor, + biometricEnvironment.createSensorPropertiesInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModel.kt index 5ce2ed756d8..0803f890a48 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModel.kt @@ -16,27 +16,27 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel -import android.util.Log import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor import kotlinx.coroutines.flow.Flow /** Models the UI state for [FingerprintEnrollConfirmationV2Fragment] */ class FingerprintEnrollConfirmationViewModel( private val navigationViewModel: FingerprintNavigationViewModel, - fingerprintInteractor: FingerprintManagerInteractor, + private val canEnrollFingerprintsInteractor: CanEnrollFingerprintsInteractor, ) : ViewModel() { /** * Indicates if the add another button is possible. This should only be true when the user is able * to enroll more fingerprints. */ - val isAddAnotherButtonVisible: Flow = fingerprintInteractor.canEnrollFingerprints + val isAddAnotherButtonVisible: Flow = + canEnrollFingerprintsInteractor.canEnrollFingerprints /** * Indicates that the user has clicked the next button and is done with fingerprint enrollment. @@ -64,7 +64,7 @@ class FingerprintEnrollConfirmationViewModel( val provider = ViewModelProvider(this[VIEW_MODEL_STORE_OWNER_KEY]!!) FingerprintEnrollConfirmationViewModel( provider[FingerprintNavigationViewModel::class], - biometricEnvironment!!.fingerprintManagerInteractor, + biometricEnvironment!!.createCanEnrollFingerprintsInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt index 3568dbdd511..9b2cdde7cef 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt @@ -27,7 +27,7 @@ import com.android.settings.SettingsApplication import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Education @@ -44,20 +44,20 @@ import kotlinx.coroutines.launch /** Models the UI state for fingerprint enroll education */ class FingerprintEnrollFindSensorViewModel( - private val navigationViewModel: FingerprintNavigationViewModel, - private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, - private val gatekeeperViewModel: FingerprintGatekeeperViewModel, - backgroundViewModel: BackgroundViewModel, - fingerprintFlowViewModel: FingerprintFlowViewModel, - accessibilityInteractor: AccessibilityInteractor, - foldStateInteractor: FoldStateInteractor, - orientationInteractor: OrientationInteractor, - fingerprintManagerInteractor: FingerprintManagerInteractor, + private val navigationViewModel: FingerprintNavigationViewModel, + private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, + private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + backgroundViewModel: BackgroundViewModel, + fingerprintFlowViewModel: FingerprintFlowViewModel, + accessibilityInteractor: AccessibilityInteractor, + foldStateInteractor: FoldStateInteractor, + orientationInteractor: OrientationInteractor, + sensorInteractor: SensorInteractor, ) : ViewModel() { /** Represents the stream of sensor type. */ val sensorType: Flow = - fingerprintManagerInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } + sensorInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } private val _isUdfps: Flow = sensorType.map { it == FingerprintSensorType.UDFPS_OPTICAL || it == FingerprintSensorType.UDFPS_ULTRASONIC @@ -216,7 +216,7 @@ class FingerprintEnrollFindSensorViewModel( biometricEnvironment.accessibilityInteractor, biometricEnvironment.foldStateInteractor, biometricEnvironment.orientationInteractor, - biometricEnvironment.fingerprintManagerInteractor, + biometricEnvironment.createSensorPropertiesInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollIntroViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollIntroViewModel.kt index 6ec204815e1..e103cbc47fc 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollIntroViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollIntroViewModel.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Introduction import com.android.systemui.biometrics.shared.model.FingerprintSensor @@ -30,13 +30,13 @@ import kotlinx.coroutines.flow.Flow /** A view model for fingerprint enroll introduction. */ class FingerprintEnrollIntroViewModel( - val navigationViewModel: FingerprintNavigationViewModel, - fingerprintFlowViewModel: FingerprintFlowViewModel, - fingerprintManagerInteractor: FingerprintManagerInteractor, + val navigationViewModel: FingerprintNavigationViewModel, + fingerprintFlowViewModel: FingerprintFlowViewModel, + sensorInteractor: SensorInteractor, ) : ViewModel() { /** Represents a stream of [FingerprintSensor] */ - val sensor: Flow = fingerprintManagerInteractor.sensorPropertiesInternal + val sensor: Flow = sensorInteractor.sensorPropertiesInternal /** Represents a stream of [FingerprintFlow] */ val fingerprintFlow: Flow = fingerprintFlowViewModel.fingerprintFlow @@ -67,7 +67,7 @@ class FingerprintEnrollIntroViewModel( FingerprintEnrollIntroViewModel( provider[FingerprintNavigationViewModel::class], provider[FingerprintFlowViewModel::class], - biometricEnvironment!!.fingerprintManagerInteractor, + biometricEnvironment!!.createSensorPropertiesInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt index 2669b8bbe5b..fb8a182ab40 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollViewModel.kt @@ -24,7 +24,8 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Education @@ -42,9 +43,10 @@ import kotlinx.coroutines.flow.update /** Represents all of the fingerprint information needed for a fingerprint enrollment process. */ class FingerprintEnrollViewModel( - private val fingerprintManagerInteractor: FingerprintManagerInteractor, - gatekeeperViewModel: FingerprintGatekeeperViewModel, - val navigationViewModel: FingerprintNavigationViewModel, + gatekeeperViewModel: FingerprintGatekeeperViewModel, + val navigationViewModel: FingerprintNavigationViewModel, + private val sensorInteractor: SensorInteractor, + private val fingerprintEnrollInteractor: EnrollFingerprintInteractor, ) : ViewModel() { /** @@ -67,7 +69,7 @@ class FingerprintEnrollViewModel( /** Represents the stream of [FingerprintSensorType] */ val sensorType: Flow = - fingerprintManagerInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } + sensorInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } /** * A flow that contains a [FingerprintEnrollViewModel] which contains the relevant information for @@ -90,7 +92,7 @@ class FingerprintEnrollViewModel( enrollReason != null && enrollOptions != null ) { - fingerprintManagerInteractor + fingerprintEnrollInteractor .enroll(hardwareAuthToken.token, enrollReason, enrollOptions) .collect { emit(it) } } @@ -137,9 +139,10 @@ class FingerprintEnrollViewModel( val biometricEnvironment = settingsApplication.biometricEnvironment val provider = ViewModelProvider(this[VIEW_MODEL_STORE_OWNER_KEY]!!) FingerprintEnrollViewModel( - biometricEnvironment!!.fingerprintManagerInteractor, provider[FingerprintGatekeeperViewModel::class], provider[FingerprintNavigationViewModel::class], + biometricEnvironment!!.createSensorPropertiesInteractor(), + biometricEnvironment!!.createFingerprintEnrollInteractor(), ) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt index b5be165056e..c2b0a0f23d9 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintGatekeeperViewModel.kt @@ -24,7 +24,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -50,7 +50,7 @@ sealed interface GatekeeperInfo { * in as a parameter to this class. */ class FingerprintGatekeeperViewModel( - private val fingerprintManagerInteractor: FingerprintManagerInteractor + private val generateChallengeInteractor: GenerateChallengeInteractor ) : ViewModel() { private var _gatekeeperInfo: MutableStateFlow = MutableStateFlow(null) @@ -78,7 +78,7 @@ class FingerprintGatekeeperViewModel( _gatekeeperInfo.update { GatekeeperInfo.Invalid } } else { viewModelScope.launch { - val res = fingerprintManagerInteractor.generateChallenge(theGatekeeperPasswordHandle!!) + val res = generateChallengeInteractor.generateChallenge(theGatekeeperPasswordHandle!!) _gatekeeperInfo.update { GatekeeperInfo.GatekeeperPasswordInfo(res.second, res.first) } if (shouldStartTimer) { startTimeout() @@ -119,7 +119,7 @@ class FingerprintGatekeeperViewModel( val settingsApplication = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as SettingsApplication val biometricEnvironment = settingsApplication.biometricEnvironment - FingerprintGatekeeperViewModel(biometricEnvironment!!.fingerprintManagerInteractor) + FingerprintGatekeeperViewModel(biometricEnvironment!!.createGenerateChallengeInteractor()) } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationViewModel.kt index caf7d2a8a9b..d9bcf7fd901 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationViewModel.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.android.settings.SettingsApplication -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Finish import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.TransitionStep @@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.update * fragments/viewmodels that want to consume these events. It should provide no additional * functionality beyond what is available in [FingerprintNavigationStep]. */ -class FingerprintNavigationViewModel(fingerprintManagerInteractor: FingerprintManagerInteractor) : +class FingerprintNavigationViewModel(sensorInteractor: SensorInteractor) : ViewModel() { private val _flowInternal: MutableStateFlow = MutableStateFlow(null) @@ -55,7 +55,7 @@ class FingerprintNavigationViewModel(fingerprintManagerInteractor: FingerprintMa combine( _flowInternal, _hasConfirmedDeviceCredential, - fingerprintManagerInteractor.sensorPropertiesInternal, + sensorInteractor.sensorPropertiesInternal, ) { flow, hasConfirmed, sensorType -> if (flow == null || sensorType == null) { return@combine null @@ -144,7 +144,7 @@ class FingerprintNavigationViewModel(fingerprintManagerInteractor: FingerprintMa val settingsApplication = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as SettingsApplication val biometricEnvironment = settingsApplication.biometricEnvironment - FingerprintNavigationViewModel(biometricEnvironment!!.fingerprintManagerInteractor) + FingerprintNavigationViewModel(biometricEnvironment!!.createSensorPropertiesInteractor()) } } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt index 4c3773bb3e9..241eaea0b28 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt @@ -35,19 +35,16 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceCategory -import com.android.internal.widget.LockPatternUtils import com.android.settings.R +import com.android.settings.SettingsApplication import com.android.settings.Utils.SETTINGS_PACKAGE_NAME import com.android.settings.biometrics.BiometricEnrollBase import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED -import com.android.settings.biometrics.GatekeeperPasswordProvider import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData @@ -223,35 +220,24 @@ class FingerprintSettingsV2Fragment : val fingerprintSensorProvider = FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope) val pressToAuthInteractor = PressToAuthInteractorImpl(context, backgroundDispatcher) - val fingerprintEnrollStateRepository = - FingerprintEnrollInteractorImpl( - requireContext().applicationContext, - fingerprintManager, - Settings, - ) - - val interactor = - FingerprintManagerInteractorImpl( - context.applicationContext, - backgroundDispatcher, - fingerprintManager, - fingerprintSensorProvider, - GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)), - fingerprintEnrollStateRepository, - ) val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN) val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L) + val application = requireActivity().application as SettingsApplication + val environment = + application.biometricEnvironment + ?: throw IllegalStateException("The biometric environment must be present") navigationViewModel = ViewModelProvider( this, FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( userId, - interactor, backgroundDispatcher, token, challenge, + environment.createFingerprintsEnrolledInteractor(), + environment.createGenerateChallengeInteractor(), ), )[FingerprintSettingsNavigationViewModel::class.java] @@ -260,9 +246,14 @@ class FingerprintSettingsV2Fragment : this, FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( userId, - interactor, backgroundDispatcher, navigationViewModel, + environment.createCanEnrollFingerprintsInteractor(), + environment.createSensorPropertiesInteractor(), + environment.createAuthenticateInteractor(), + environment.createRenameFingerprintInteractor(), + environment.createRemoveFingerprintInteractor(), + environment.createFingerprintsEnrolledInteractor(), ), )[FingerprintSettingsViewModel::class.java] diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsNavigationViewModel.kt index 8a694aeac91..73b2b1cd3d3 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsNavigationViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsNavigationViewModel.kt @@ -21,7 +21,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.biometrics.BiometricEnrollBase -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -33,10 +34,11 @@ import kotlinx.coroutines.launch /** A Viewmodel that represents the navigation of the FingerprintSettings activity. */ class FingerprintSettingsNavigationViewModel( private val userId: Int, - private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val backgroundDispatcher: CoroutineDispatcher, tokenInit: ByteArray?, challengeInit: Long?, + private val enrolledFingerprintsInteractor: EnrolledFingerprintsInteractor, + private val generateChallengeInteractor: GenerateChallengeInteractor, ) : ViewModel() { private var token = tokenInit @@ -52,7 +54,7 @@ class FingerprintSettingsNavigationViewModel( _nextStep.update { LaunchConfirmDeviceCredential(userId) } } else { viewModelScope.launch { - if (fingerprintManagerInteractor.enrolledFingerprints.last()?.isEmpty() == true) { + if (enrolledFingerprintsInteractor.enrolledFingerprints.last()?.isEmpty() == true) { _nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) } } else { showSettingsHelper() @@ -148,13 +150,13 @@ class FingerprintSettingsNavigationViewModel( } private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) { - fingerprintManagerInteractor.enrolledFingerprints.collect { + enrolledFingerprintsInteractor.enrolledFingerprints.collect { if (it?.isEmpty() == true) { _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) } } else { viewModelScope.launch(backgroundDispatcher) { val challengePair = - fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!) + generateChallengeInteractor.generateChallenge(gateKeeperPasswordHandle!!) challenge = challengePair.first token = challengePair.second @@ -174,10 +176,11 @@ class FingerprintSettingsNavigationViewModel( class FingerprintSettingsNavigationModelFactory( private val userId: Int, - private val interactor: FingerprintManagerInteractor, private val backgroundDispatcher: CoroutineDispatcher, private val token: ByteArray?, private val challenge: Long?, + private val enrolledFingerprintsInteractor: EnrolledFingerprintsInteractor, + private val generateChallengeInteractor: GenerateChallengeInteractor, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -185,10 +188,11 @@ class FingerprintSettingsNavigationViewModel( return FingerprintSettingsNavigationViewModel( userId, - interactor, backgroundDispatcher, token, challenge, + enrolledFingerprintsInteractor, + generateChallengeInteractor, ) as T } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt index cf8c527d6d4..c306c7870b9 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt @@ -21,7 +21,12 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.AuthenitcateInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -49,9 +54,14 @@ private const val DEBUG = false /** Models the UI state for fingerprint settings. */ class FingerprintSettingsViewModel( private val userId: Int, - private val fingerprintManagerInteractor: FingerprintManagerInteractor, private val backgroundDispatcher: CoroutineDispatcher, private val navigationViewModel: FingerprintSettingsNavigationViewModel, + private val canEnrollFingerprintsInteractor: CanEnrollFingerprintsInteractor, + private val sensorInteractor: SensorInteractor, + private val authenticateInteractor: AuthenitcateInteractor, + private val renameFingerprintInteractor: RenameFingerprintInteractor, + private val removeFingerprintInteractor: RemoveFingerprintInteractor, + private val enrolledFingerprintsInteractor: EnrolledFingerprintsInteractor, ) : ViewModel() { private val _enrolledFingerprints: MutableStateFlow?> = MutableStateFlow(null) @@ -62,19 +72,18 @@ class FingerprintSettingsViewModel( /** Represents the stream of the information of "Add Fingerprint" preference. */ val addFingerprintPrefInfo: Flow> = - _enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform { - emit( - Pair( - fingerprintManagerInteractor.canEnrollFingerprints.first(), - fingerprintManagerInteractor.maxEnrollableFingerprints.first(), - ) - ) + _enrolledFingerprints.filterOnlyWhenSettingsIsShown().combine( + canEnrollFingerprintsInteractor.canEnrollFingerprints + ) { _, canEnrollFingerprints -> + Pair(canEnrollFingerprints, canEnrollFingerprintsInteractor.maxFingerprintsEnrollable()) } /** Represents the stream of visibility of sfps preference. */ val isSfpsPrefVisible: Flow = - _enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform { - emit(fingerprintManagerInteractor.hasSideFps() == true && !it.isNullOrEmpty()) + _enrolledFingerprints.filterOnlyWhenSettingsIsShown().combine(sensorInteractor.hasSideFps) { + fingerprints, + hasSideFps -> + hasSideFps && !fingerprints.isNullOrEmpty() } private val _isShowingDialog: MutableStateFlow = MutableStateFlow(null) @@ -90,10 +99,10 @@ class FingerprintSettingsViewModel( private val _consumerShouldAuthenticate: MutableStateFlow = MutableStateFlow(false) private val _fingerprintSensorType: Flow = - fingerprintManagerInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } + sensorInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType } private val _sensorNullOrEmpty: Flow = - fingerprintManagerInteractor.sensorPropertiesInternal.map { it == null } + sensorInteractor.sensorPropertiesInternal.map { it == null } private val _isLockedOut: MutableStateFlow = MutableStateFlow(null) @@ -172,7 +181,7 @@ class FingerprintSettingsViewModel( while (it && navigationViewModel.nextStep.value is ShowSettings) { Log.d(TAG, "canAuthenticate authing") attemptingAuth() - when (val authAttempt = fingerprintManagerInteractor.authenticate()) { + when (val authAttempt = authenticateInteractor.authenticate()) { is FingerprintAuthAttemptModel.Success -> { onAuthSuccess(authAttempt) emit(authAttempt) @@ -243,7 +252,7 @@ class FingerprintSettingsViewModel( /** A request to delete a fingerprint */ fun deleteFingerprint(fp: FingerprintData) { viewModelScope.launch(backgroundDispatcher) { - if (fingerprintManagerInteractor.removeFingerprint(fp)) { + if (removeFingerprintInteractor.removeFingerprint(fp)) { updateEnrolledFingerprints() } } @@ -252,7 +261,7 @@ class FingerprintSettingsViewModel( /** A request to rename a fingerprint */ fun renameFingerprint(fp: FingerprintData, newName: String) { viewModelScope.launch { - fingerprintManagerInteractor.renameFingerprint(fp, newName) + renameFingerprintInteractor.renameFingerprint(fp, newName) updateEnrolledFingerprints() } } @@ -271,7 +280,7 @@ class FingerprintSettingsViewModel( } private suspend fun updateEnrolledFingerprints() { - _enrolledFingerprints.update { fingerprintManagerInteractor.enrolledFingerprints.first() } + _enrolledFingerprints.update { enrolledFingerprintsInteractor.enrolledFingerprints.first() } } /** Used to indicate whether the consumer of the view model is ready for authentication. */ @@ -288,9 +297,14 @@ class FingerprintSettingsViewModel( class FingerprintSettingsViewModelFactory( private val userId: Int, - private val interactor: FingerprintManagerInteractor, private val backgroundDispatcher: CoroutineDispatcher, private val navigationViewModel: FingerprintSettingsNavigationViewModel, + private val canEnrollFingerprintsInteractor: CanEnrollFingerprintsInteractor, + private val sensorInteractor: SensorInteractor, + private val authenticateInteractor: AuthenitcateInteractor, + private val renameFingerprintInteractor: RenameFingerprintInteractor, + private val removeFingerprintInteractor: RemoveFingerprintInteractor, + private val enrolledFingerprintsInteractor: EnrolledFingerprintsInteractor, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -298,9 +312,14 @@ class FingerprintSettingsViewModel( return FingerprintSettingsViewModel( userId, - interactor, backgroundDispatcher, navigationViewModel, + canEnrollFingerprintsInteractor, + sensorInteractor, + authenticateInteractor, + renameFingerprintInteractor, + removeFingerprintInteractor, + enrolledFingerprintsInteractor, ) as T } diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt index 19433f3cb19..e7fc3ed3d66 100644 --- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt +++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt @@ -111,9 +111,10 @@ class Injector(step: FingerprintNavigationStep.UiStep) { var fingerprintEnrollViewModel = FingerprintEnrollViewModel( - fingerprintManagerInteractor, gatekeeperViewModel, navigationViewModel, + fingerprintManagerInteractor, + fingerprintManagerInteractor, ) var fingerprintEnrollEnrollingViewModel = diff --git a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt index 52df724b82d..f61a3d3a02e 100644 --- a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt +++ b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt @@ -22,7 +22,14 @@ import android.hardware.biometrics.SensorProperties import android.hardware.fingerprint.FingerprintEnrollOptions import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.AuthenitcateInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.SensorInteractor import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel @@ -35,7 +42,15 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf /** Fake to be used by other classes to easily fake the FingerprintManager implementation. */ -class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { +class FakeFingerprintManagerInteractor : + AuthenitcateInteractor, + CanEnrollFingerprintsInteractor, + EnrolledFingerprintsInteractor, + EnrollFingerprintInteractor, + GenerateChallengeInteractor, + RemoveFingerprintInteractor, + RenameFingerprintInteractor, + SensorInteractor { var enrollableFingerprints: Int = 5 var enrolledFingerprintsInternal: MutableList = mutableListOf() @@ -67,19 +82,22 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { override val enrolledFingerprints: Flow> = flow { emit(enrolledFingerprintsInternal) } - override val canEnrollFingerprints: Flow = flow { emit(enrolledFingerprintsInternal.size < enrollableFingerprints) } - override val sensorPropertiesInternal: Flow = flow { emit(sensorProp) } + override fun maxFingerprintsEnrollable(): Int { + return enrollableFingerprints + } - override val maxEnrollableFingerprints: Flow = flow { emit(enrollableFingerprints) } + override val sensorPropertiesInternal: Flow = flow { emit(sensorProp) } + override val hasSideFps: Flow = + flowOf(sensorProp.sensorType == FingerprintSensorType.POWER_BUTTON) override suspend fun enroll( hardwareAuthToken: ByteArray?, enrollReason: EnrollReason, - fingerprintEnrollOptions: FingerprintEnrollOptions + fingerprintEnrollOptions: FingerprintEnrollOptions, ): Flow = flowOf(*enrollStateViewModel.toTypedArray()) override suspend fun removeFingerprint(fp: FingerprintData): Boolean { @@ -92,7 +110,4 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor { } } - override suspend fun hasSideFps(): Boolean { - return sensorProp.sensorType == FingerprintSensorType.POWER_BUTTON - } } diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt index 67a5957b9f5..691b6112bf7 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt @@ -16,7 +16,6 @@ package com.android.settings.fingerprint2.domain.interactor -import android.content.Context import android.content.Intent import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.SensorLocationInternal @@ -30,23 +29,37 @@ import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.CancellationSignal import android.os.Handler -import androidx.test.core.app.ApplicationProvider import com.android.settings.biometrics.GatekeeperPasswordProvider +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepositoryImpl import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl -import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl -import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSettingsRepositoryImpl +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo +import com.android.settings.biometrics.fingerprint2.domain.interactor.AuthenticateInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.CanEnrollFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollFingerprintInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrolledFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.GenerateChallengeInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.RemoveFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.RenameFingerprintsInteractorImpl +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnrollFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrollFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.EnrolledFingerprintsInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.GenerateChallengeInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RemoveFingerprintInteractor +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.RenameFingerprintInteractor import com.android.settings.biometrics.fingerprint2.lib.model.Default import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData +import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow import com.android.settings.password.ChooseLockSettingsHelper import com.android.systemui.biometrics.shared.model.FingerprintSensor import com.android.systemui.biometrics.shared.model.toFingerprintSensor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.last import kotlinx.coroutines.launch @@ -75,13 +88,28 @@ import org.mockito.stubbing.OngoingStubbing class FingerprintManagerInteractorTest { @JvmField @Rule var rule = MockitoJUnit.rule() - private lateinit var underTest: FingerprintManagerInteractor - private var context: Context = ApplicationProvider.getApplicationContext() + private lateinit var enrolledFingerprintsInteractorUnderTest: EnrolledFingerprintsInteractor + private lateinit var generateChallengeInteractorUnderTest: GenerateChallengeInteractor + private lateinit var removeFingerprintsInteractorUnderTest: RemoveFingerprintInteractor + private lateinit var renameFingerprintsInteractorUnderTest: RenameFingerprintInteractor + private lateinit var authenticateInteractorImplUnderTest: AuthenticateInteractorImpl + private lateinit var canEnrollFingerprintsInteractorUnderTest: CanEnrollFingerprintsInteractor + private lateinit var enrollInteractorUnderTest: EnrollFingerprintInteractor + + private val userId = 0 private var backgroundDispatcher = StandardTestDispatcher() @Mock private lateinit var fingerprintManager: FingerprintManager @Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider private var testScope = TestScope(backgroundDispatcher) + private var backgroundScope = testScope.backgroundScope + private val flow: FingerprintFlow = Default + private val maxFingerprints = 5 + private val currUser = MutableStateFlow(0) + private val userRepo = + object : UserRepo { + override val currentUser: Flow = currUser + } @Before fun setup() { @@ -89,7 +117,7 @@ class FingerprintManagerInteractorTest { FingerprintSensorPropertiesInternal( 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, + maxFingerprints, listOf(), FingerprintSensorProperties.TYPE_POWER_BUTTON, false /* halControlsIllumination */, @@ -97,20 +125,37 @@ class FingerprintManagerInteractorTest { listOf(SensorLocationInternal.DEFAULT), ) .toFingerprintSensor() + val fingerprintSensorRepository = object : FingerprintSensorRepository { override val fingerprintSensor: Flow = flowOf(sensor) + override val hasSideFps: Flow = flowOf(false) } - underTest = - FingerprintManagerInteractorImpl( - context, - backgroundDispatcher, + val settingsRepository = FingerprintSettingsRepositoryImpl(maxFingerprints) + val fingerprintEnrollmentRepository = + FingerprintEnrollmentRepositoryImpl( fingerprintManager, - fingerprintSensorRepository, - gateKeeperPasswordProvider, - FingerprintEnrollInteractorImpl(context, fingerprintManager, Default), + userRepo, + settingsRepository, + backgroundDispatcher, + backgroundScope, ) + + enrolledFingerprintsInteractorUnderTest = + EnrolledFingerprintsInteractorImpl(fingerprintManager, userId) + generateChallengeInteractorUnderTest = + GenerateChallengeInteractorImpl(fingerprintManager, userId, gateKeeperPasswordProvider) + removeFingerprintsInteractorUnderTest = + RemoveFingerprintsInteractorImpl(fingerprintManager, userId) + renameFingerprintsInteractorUnderTest = + RenameFingerprintsInteractorImpl(fingerprintManager, userId, backgroundDispatcher) + authenticateInteractorImplUnderTest = AuthenticateInteractorImpl(fingerprintManager, userId) + + canEnrollFingerprintsInteractorUnderTest = + CanEnrollFingerprintsInteractorImpl(fingerprintEnrollmentRepository) + + enrollInteractorUnderTest = EnrollFingerprintInteractorImpl(userId, fingerprintManager, flow) } @Test @@ -119,7 +164,8 @@ class FingerprintManagerInteractorTest { whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList()) val emptyFingerprintList: List = emptyList() - assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList) + assertThat(enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last()) + .isEqualTo(emptyFingerprintList) } @Test @@ -129,7 +175,7 @@ class FingerprintManagerInteractorTest { val fingerprintList: List = listOf(expected) whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) - val list = underTest.enrolledFingerprints.last() + val list = enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last() assertThat(list!!.size).isEqualTo(fingerprintList.size) val actual = list[0] assertThat(actual.name).isEqualTo(expected.name) @@ -138,24 +184,51 @@ class FingerprintManagerInteractorTest { } @Test - fun testCanEnrollFingerprint() = + fun testCanEnrollFingerprintSucceeds() = testScope.runTest { - val fingerprintList1: List = + val fingerprintList: List = listOf( - Fingerprint("Finger 1,", 2, 3L), - Fingerprint("Finger 2,", 3, 3L), - Fingerprint("Finger 3,", 4, 3L), + Fingerprint("Finger 1", 2, 3L), + Fingerprint("Finger 2", 3, 3L), + Fingerprint("Finger 3", 4, 3L), ) - val fingerprintList2: List = - fingerprintList1.plus( - listOf(Fingerprint("Finger 4,", 5, 3L), Fingerprint("Finger 5,", 6, 3L)) - ) - whenever(fingerprintManager.getEnrolledFingerprints(anyInt())) - .thenReturn(fingerprintList1) - .thenReturn(fingerprintList2) + whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) - assertThat(underTest.canEnrollFingerprints.last()).isTrue() - assertThat(underTest.canEnrollFingerprints.last()).isFalse() + var result: Boolean? = null + val job = + testScope.launch { + canEnrollFingerprintsInteractorUnderTest.canEnrollFingerprints.collect { result = it } + } + + runCurrent() + job.cancelAndJoin() + + assertThat(result).isTrue() + } + + @Test + fun testCanEnrollFingerprintFails() = + testScope.runTest { + val fingerprintList: List = + listOf( + Fingerprint("Finger 1", 2, 3L), + Fingerprint("Finger 2", 3, 3L), + Fingerprint("Finger 3", 4, 3L), + Fingerprint("Finger 4", 5, 3L), + Fingerprint("Finger 5", 6, 3L), + ) + whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) + + var result: Boolean? = null + val job = + testScope.launch { + canEnrollFingerprintsInteractorUnderTest.canEnrollFingerprints.collect { result = it } + } + + runCurrent() + job.cancelAndJoin() + + assertThat(result).isFalse() } @Test @@ -178,7 +251,8 @@ class FingerprintManagerInteractorTest { argumentCaptor() var result: Pair? = null - val job = testScope.launch { result = underTest.generateChallenge(1L) } + val job = + testScope.launch { result = generateChallengeInteractorUnderTest.generateChallenge(1L) } runCurrent() verify(fingerprintManager).generateChallenge(anyInt(), capture(generateChallengeCallback)) @@ -201,7 +275,10 @@ class FingerprintManagerInteractorTest { var result: Boolean? = null val job = - testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) } + testScope.launch { + result = + removeFingerprintsInteractorUnderTest.removeFingerprint(fingerprintViewModelToRemove) + } runCurrent() verify(fingerprintManager) @@ -224,7 +301,10 @@ class FingerprintManagerInteractorTest { var result: Boolean? = null val job = - testScope.launch { result = underTest.removeFingerprint(fingerprintViewModelToRemove) } + testScope.launch { + result = + removeFingerprintsInteractorUnderTest.removeFingerprint(fingerprintViewModelToRemove) + } runCurrent() verify(fingerprintManager) @@ -246,7 +326,7 @@ class FingerprintManagerInteractorTest { testScope.runTest { val fingerprintToRename = FingerprintData("Finger 2", 1, 2L) - underTest.renameFingerprint(fingerprintToRename, "Woo") + renameFingerprintsInteractorUnderTest.renameFingerprint(fingerprintToRename, "Woo") verify(fingerprintManager).rename(eq(fingerprintToRename.fingerId), anyInt(), safeEq("Woo")) } @@ -257,7 +337,7 @@ class FingerprintManagerInteractorTest { val fingerprint = Fingerprint("Woooo", 100, 101L) var result: FingerprintAuthAttemptModel? = null - val job = launch { result = underTest.authenticate() } + val job = launch { result = authenticateInteractorImplUnderTest.authenticate() } val authCallback: ArgumentCaptor = argumentCaptor() @@ -284,7 +364,7 @@ class FingerprintManagerInteractorTest { fun testAuth_lockout() = testScope.runTest { var result: FingerprintAuthAttemptModel? = null - val job = launch { result = underTest.authenticate() } + val job = launch { result = authenticateInteractorImplUnderTest.authenticate() } val authCallback: ArgumentCaptor = argumentCaptor() @@ -314,7 +394,7 @@ class FingerprintManagerInteractorTest { val token = byteArrayOf(5, 3, 2) var result: FingerEnrollState? = null val job = launch { - underTest + enrollInteractorUnderTest .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) .collect { result = it } } @@ -343,7 +423,7 @@ class FingerprintManagerInteractorTest { val token = byteArrayOf(5, 3, 2) var result: FingerEnrollState? = null val job = launch { - underTest + enrollInteractorUnderTest .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) .collect { result = it } } @@ -372,7 +452,7 @@ class FingerprintManagerInteractorTest { val token = byteArrayOf(5, 3, 2) var result: FingerEnrollState? = null val job = launch { - underTest + enrollInteractorUnderTest .enroll(token, EnrollReason.FindSensor, FingerprintEnrollOptions.Builder().build()) .collect { result = it } } diff --git a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt index 9662c39d94a..04cece83cd0 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/enrollment/viewmodel/FingerprintEnrollFindSensorViewModelV2Test.kt @@ -99,9 +99,10 @@ class FingerprintEnrollFindSensorViewModelV2Test { backgroundViewModel.inForeground() enrollViewModel = FingerprintEnrollViewModel( - fakeFingerprintManagerInteractor, gatekeeperViewModel, navigationViewModel, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) accessibilityInteractor = object : AccessibilityInteractor { diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt index 46e883af33d..53f472691d6 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/modules/enrolling/rfps/viewmodel/RFPSIconTouchViewModelTest.kt @@ -49,8 +49,7 @@ class RFPSIconTouchViewModelTest { fun setup() { Dispatchers.setMain(backgroundDispatcher) testScope = TestScope(backgroundDispatcher) - rfpsIconTouchViewModel = - RFPSIconTouchViewModel() + rfpsIconTouchViewModel = RFPSIconTouchViewModel() } @After diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt index c475cc47faa..cf2deec5bac 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModelTest.kt @@ -88,9 +88,10 @@ class FingerprintEnrollEnrollingViewModelTest { backgroundViewModel.inForeground() val fingerprintEnrollViewModel = FingerprintEnrollViewModel( - fakeFingerprintManagerInteractor, gateKeeperViewModel, navigationViewModel, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) enrollEnrollingViewModel = FingerprintEnrollEnrollingViewModel(fingerprintEnrollViewModel, backgroundViewModel) diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt index 201fffa608a..88f76dd23f2 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsNavigationViewModelTest.kt @@ -67,10 +67,11 @@ class FingerprintSettingsNavigationViewModelTest { underTest = FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, null, null, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsNavigationViewModel::class.java) } @@ -272,10 +273,11 @@ class FingerprintSettingsNavigationViewModelTest { underTest = FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, token, challenge, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsNavigationViewModel::class.java) @@ -299,10 +301,11 @@ class FingerprintSettingsNavigationViewModelTest { underTest = FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, token, challenge, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsNavigationViewModel::class.java) @@ -331,10 +334,11 @@ class FingerprintSettingsNavigationViewModelTest { underTest = FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, token, challenge, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsNavigationViewModel::class.java) diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt index 1618e16d904..79163d9a590 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/settings/FingerprintSettingsViewModelTest.kt @@ -73,19 +73,25 @@ class FingerprintSettingsViewModelTest { navigationViewModel = FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, null, null, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsNavigationViewModel::class.java) underTest = FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( defaultUserId, - fakeFingerprintManagerInteractor, backgroundDispatcher, navigationViewModel, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, ) .create(FingerprintSettingsViewModel::class.java) } @@ -114,14 +120,7 @@ class FingerprintSettingsViewModelTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(FingerprintData("a", 1, 3L)) - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } @@ -156,14 +155,7 @@ class FingerprintSettingsViewModelTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(FingerprintData("a", 1, 3L)) - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var authAttempt: FingerprintAuthAttemptModel? = null val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } } @@ -198,14 +190,7 @@ class FingerprintSettingsViewModelTest { val success = FingerprintAuthAttemptModel.Success(1) fakeFingerprintManagerInteractor.authenticateAttempt = success - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var authAttempt: FingerprintAuthAttemptModel? = null @@ -225,14 +210,7 @@ class FingerprintSettingsViewModelTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete) - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var dialog: PreferenceViewModel? = null val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } } @@ -261,14 +239,7 @@ class FingerprintSettingsViewModelTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToRename) - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var dialog: PreferenceViewModel? = null val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } } @@ -299,14 +270,7 @@ class FingerprintSettingsViewModelTest { fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete) - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() var dialog: PreferenceViewModel? = null val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } } @@ -390,6 +354,22 @@ class FingerprintSettingsViewModelTest { assertThat(authAttempt).isEqualTo(null) } + private fun recreateSettingsViewModel() { + underTest = + FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( + defaultUserId, + backgroundDispatcher, + navigationViewModel, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + fakeFingerprintManagerInteractor, + ) + .create(FingerprintSettingsViewModel::class.java) + } + private fun setupAuth(): MutableList { fakeFingerprintManagerInteractor.sensorProp = FingerprintSensorPropertiesInternal( @@ -409,14 +389,7 @@ class FingerprintSettingsViewModelTest { val success = FingerprintAuthAttemptModel.Success(1) fakeFingerprintManagerInteractor.authenticateAttempt = success - underTest = - FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory( - defaultUserId, - fakeFingerprintManagerInteractor, - backgroundDispatcher, - navigationViewModel, - ) - .create(FingerprintSettingsViewModel::class.java) + recreateSettingsViewModel() return fingerprints } From 2f4c6f7b7a5287a79b05ca7ea9e273b432f5d6b2 Mon Sep 17 00:00:00 2001 From: josephpv Date: Tue, 13 Aug 2024 22:25:52 +0000 Subject: [PATCH 2/9] Removing TODO from test In controller test, simulating profile with hideIndQuietMode set to true in by mocking AppEntry is not possible. updateAppList() method is assumed to already receive a filtered list of apps based on the profile's quiet mode status which is called from onRebuildComplete(). Corresponding filters for this page has test in the underlying settingsLib ApplicationsStateTest file Bug: 26013153 Test: Manual Flag: EXEMPT only test file Change-Id: I09ea3b279c40e0ee4806a80646a5dfcd3d3f784b --- .../zen/ZenModeAddBypassingAppsPreferenceControllerTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/robotests/src/com/android/settings/notification/zen/ZenModeAddBypassingAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/zen/ZenModeAddBypassingAppsPreferenceControllerTest.java index 27df89046f4..2569ca3592f 100644 --- a/tests/robotests/src/com/android/settings/notification/zen/ZenModeAddBypassingAppsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/zen/ZenModeAddBypassingAppsPreferenceControllerTest.java @@ -162,7 +162,4 @@ public class ZenModeAddBypassingAppsPreferenceControllerTest { assertThat(pref.getKey()).isEqualTo( ZenModeAddBypassingAppsPreferenceController.KEY_NO_APPS); } - - // TODO(b/331624810): Add tests to verify updateAppList() when the filter is - // ApplicationsState.FILTER_ENABLED_NOT_QUIET } From 23258c2b100340bbb66374f1bf7cefed503368eb Mon Sep 17 00:00:00 2001 From: Isaac Chai Date: Thu, 15 Aug 2024 19:36:54 +0000 Subject: [PATCH 3/9] fix(Color correction): Display default value in slider to match the radio button The default value for display in the radio buttons were hard-coded and was not being reflected to the slider correctly. Bug: 359379399 Test: locally tested + unit tests added Flag: com.android.server.accessibility.enable_color_correction_saturation Change-Id: I95bd0ae1d32561dbbf6b4e87efe70595c9566de8 --- .../DaltonizerPreferenceUtil.java | 59 +++++++++++++++++ ...onizerRadioButtonPreferenceController.java | 17 +---- ...SaturationSeekbarPreferenceController.java | 12 ++-- .../ToggleDaltonizerPreferenceFragment.java | 4 +- ...rationSeekbarPreferenceControllerTest.java | 66 +++++++++++-------- 5 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java diff --git a/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java b/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java new file mode 100644 index 00000000000..459dbb98451 --- /dev/null +++ b/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 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.settings.accessibility; + +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + +import android.content.ContentResolver; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + +import com.google.common.primitives.Ints; + +/** + * Utility class for retrieving accessibility daltonizer related values in secure settings. + */ +public class DaltonizerPreferenceUtil { + + /** + * Return the daltonizer display mode stored in + * {@link Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER}. + * By default it returns {@link DALTONIZER_CORRECT_DEUTERANOMALY}. + */ + public static int getSecureAccessibilityDaltonizerValue(ContentResolver resolver) { + final String daltonizerStringValue = Settings.Secure.getString( + resolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER); + if (daltonizerStringValue == null) { + return AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY; + } + final Integer daltonizerIntValue = Ints.tryParse(daltonizerStringValue); + return daltonizerIntValue == null ? AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY + : daltonizerIntValue; + } + + /** + * Returns the daltonizer enabled value in + * {@link Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED}. + * By default it returns false. + */ + public static boolean isSecureAccessibilityDaltonizerEnabled(ContentResolver resolver) { + return Settings.Secure.getInt( + resolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + OFF) == ON; + } +} diff --git a/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java b/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java index 296536c2faf..5a8c710b55c 100644 --- a/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java +++ b/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java @@ -24,7 +24,6 @@ import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.view.View; -import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; @@ -36,8 +35,6 @@ import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.widget.SelectorWithWidgetPreference; -import com.google.common.primitives.Ints; - import java.util.HashMap; import java.util.Map; @@ -70,17 +67,6 @@ public class DaltonizerRadioButtonPreferenceController extends BasePreferenceCon }; } - protected static int getSecureAccessibilityDaltonizerValue(ContentResolver resolver) { - final String daltonizerStringValue = Settings.Secure.getString( - resolver, DALTONIZER_TYPE_SETTINGS_KEY); - if (daltonizerStringValue == null) { - return AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY; - } - final Integer daltonizerIntValue = Ints.tryParse(daltonizerStringValue); - return daltonizerIntValue == null ? AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY - : daltonizerIntValue; - } - private Map getDaltonizerValueToKeyMap() { if (mAccessibilityDaltonizerKeyToValueMap.isEmpty()) { @@ -123,7 +109,8 @@ public class DaltonizerRadioButtonPreferenceController extends BasePreferenceCon } private int getAccessibilityDaltonizerValue() { - final int daltonizerValue = getSecureAccessibilityDaltonizerValue(mContentResolver); + final int daltonizerValue = + DaltonizerPreferenceUtil.getSecureAccessibilityDaltonizerValue(mContentResolver); return daltonizerValue; } diff --git a/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java b/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java index 29971854e6b..6a9977ad2d7 100644 --- a/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java +++ b/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java @@ -15,6 +15,9 @@ */ package com.android.settings.accessibility; +import static com.android.settings.accessibility.DaltonizerPreferenceUtil.isSecureAccessibilityDaltonizerEnabled; +import static com.android.settings.accessibility.DaltonizerPreferenceUtil.getSecureAccessibilityDaltonizerValue; + import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -158,14 +161,11 @@ public class DaltonizerSaturationSeekbarPreferenceController } private boolean shouldSeekBarEnabled() { - int enabled = Settings.Secure.getInt( - mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0); - int mode = Settings.Secure.getInt( - mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, -1); + boolean enabled = isSecureAccessibilityDaltonizerEnabled(mContentResolver); + int mode = getSecureAccessibilityDaltonizerValue(mContentResolver); - // enabled == 0 is disabled and also default. // mode == 0 is gray scale where saturation level isn't applicable. // mode == -1 is disabled and also default. - return enabled != 0 && mode != -1 && mode != 0; + return enabled && mode != -1 && mode != 0; } } diff --git a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java index 52f1695cbe5..86ddd71e262 100644 --- a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java @@ -21,6 +21,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; import static com.android.settings.accessibility.AccessibilityUtil.State.ON; +import static com.android.settings.accessibility.DaltonizerPreferenceUtil.isSecureAccessibilityDaltonizerEnabled; import android.app.settings.SettingsEnums; import android.content.ComponentName; @@ -145,7 +146,8 @@ public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceF @Override protected void onPreferenceToggled(String preferenceKey, boolean enabled) { - final boolean isEnabled = Settings.Secure.getInt(getContentResolver(), ENABLED, OFF) == ON; + final boolean isEnabled = + isSecureAccessibilityDaltonizerEnabled(getContentResolver()); if (enabled == isEnabled) { return; } diff --git a/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java index 5fd11f910fa..67970613822 100644 --- a/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java @@ -58,8 +58,6 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { private ContentResolver mContentResolver; private DaltonizerSaturationSeekbarPreferenceController mController; - private int mOriginalSaturationLevel = -1; - private PreferenceScreen mScreen; private LifecycleOwner mLifecycleOwner; private Lifecycle mLifecycle; @@ -73,10 +71,6 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { public void setup() { Context context = ApplicationProvider.getApplicationContext(); mContentResolver = context.getContentResolver(); - mOriginalSaturationLevel = Settings.Secure.getInt( - mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, - 7); mPreference = new SeekBarPreference(context); mPreference.setKey(ToggleDaltonizerPreferenceFragment.KEY_SATURATION); @@ -92,10 +86,18 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { @After public void cleanup() { - Settings.Secure.putInt( + Settings.Secure.putString( + mContentResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, + null); + Settings.Secure.putString( + mContentResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + null); + Settings.Secure.putString( mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, - mOriginalSaturationLevel); + null); } @Test @@ -111,6 +113,22 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION) + public void getAvailabilityStatus_defaultSettings_unavailable() { + // By default enabled == false. + assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION) + public void getAvailabilityStatus_enabledDefaultDisplayMode_available() { + setDaltonizerEnabled(1); + + // By default display mode is deuteranomaly. + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + @Test @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION) public void getAvailabilityStatus_flagEnabledProtanEnabled_available() { @@ -306,10 +324,7 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { mLifecycle.addObserver(mController); mLifecycle.handleLifecycleEvent(ON_RESUME); - Settings.Secure.putInt( - mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, - 1); + setDaltonizerEnabled(1); shadowOf(Looper.getMainLooper()).idle(); assertThat(mPreference.isEnabled()).isTrue(); @@ -324,10 +339,7 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { mLifecycle.addObserver(mController); mLifecycle.handleLifecycleEvent(ON_RESUME); - Settings.Secure.putInt( - mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, - 0); + setDaltonizerEnabled(0); shadowOf(Looper.getMainLooper()).idle(); assertThat(mPreference.isEnabled()).isFalse(); @@ -342,10 +354,7 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { mLifecycle.addObserver(mController); mLifecycle.handleLifecycleEvent(ON_RESUME); - Settings.Secure.putInt( - mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, - 0); + setDaltonizerDisplay(0); shadowOf(Looper.getMainLooper()).idle(); assertThat(mPreference.isEnabled()).isFalse(); @@ -361,23 +370,28 @@ public class DaltonizerSaturationSeekbarPreferenceControllerTest { mLifecycle.handleLifecycleEvent(ON_STOP); // enabled. - Settings.Secure.putInt( - mContentResolver, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, - 1); + setDaltonizerEnabled(1); shadowOf(Looper.getMainLooper()).idle(); assertThat(mPreference.isEnabled()).isFalse(); } private void setDaltonizerMode(int enabled, int mode) { + setDaltonizerEnabled(enabled); + setDaltonizerDisplay(mode); + } + + private void setDaltonizerEnabled(int enabled) { Settings.Secure.putInt( mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, enabled); - Settings.Secure.putInt( + } + + private void setDaltonizerDisplay(int mode) { + Settings.Secure.putString( mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, - mode); + Integer.toString(mode)); } } From 405251e52fb0c44bd3c163da08097139d63942ef Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Thu, 15 Aug 2024 17:26:39 +0800 Subject: [PATCH 4/9] Add flag for the Legal information page migration Test: compile Bug: 360061554 Flag: EXEMPT com.android.settings.flags.catalyst_legal_information Change-Id: Ic5441b27c2663351990b355315cbdb6a11a22aee --- aconfig/settings_flag_declarations.aconfig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aconfig/settings_flag_declarations.aconfig b/aconfig/settings_flag_declarations.aconfig index 2c8eadeff4e..0007bbffb75 100644 --- a/aconfig/settings_flag_declarations.aconfig +++ b/aconfig/settings_flag_declarations.aconfig @@ -49,3 +49,10 @@ flag { description: "Flag to gate support of injected preference icons containing raw data" bug: "351884562" } + +flag { + name: "catalyst_legal_information" + namespace: "android_settings" + description: "This flag controls the About phone > Legal information page migration" + bug: "323791114" +} From 05e857de6a62d18c0cbdb9c00e4d557ca387f7b7 Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Fri, 16 Aug 2024 12:33:51 +0800 Subject: [PATCH 5/9] Migrate Spinner on Battery Usage to settingsLib Spinner widget. Bug: 359429437 Test: visual Test: atest BatteryUsageBreakdownControllerTest Flag: EXEMPT bug fix Change-Id: I71bd1f31db302d407603c71d1a1163ed22fafed8 --- res/layout/preference_spinner.xml | 24 ---- res/xml/power_usage_advanced.xml | 2 +- .../BatteryUsageBreakdownController.java | 39 ++++- .../batteryusage/SpinnerPreference.java | 134 ------------------ .../BatteryUsageBreakdownControllerTest.java | 11 ++ .../batteryusage/SpinnerPreferenceTest.java | 82 ----------- 6 files changed, 46 insertions(+), 246 deletions(-) delete mode 100644 res/layout/preference_spinner.xml delete mode 100644 src/com/android/settings/fuelgauge/batteryusage/SpinnerPreference.java delete mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreferenceTest.java diff --git a/res/layout/preference_spinner.xml b/res/layout/preference_spinner.xml deleted file mode 100644 index 41293033841..00000000000 --- a/res/layout/preference_spinner.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/res/xml/power_usage_advanced.xml b/res/xml/power_usage_advanced.xml index 816e1974cb0..f2c3d878e19 100644 --- a/res/xml/power_usage_advanced.xml +++ b/res/xml/power_usage_advanced.xml @@ -57,7 +57,7 @@ "com.android.settings.fuelgauge.batteryusage.BatteryUsageBreakdownController" settings:isPreferenceVisible="false"> - diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java index f451f0a7225..09940b3d7f5 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownController.java @@ -20,6 +20,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; @@ -46,9 +47,13 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreate; import com.android.settingslib.core.lifecycle.events.OnDestroy; import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.SettingsSpinnerAdapter; +import com.android.settingslib.widget.SettingsSpinnerPreference; import java.util.ArrayList; import java.util.List; @@ -58,7 +63,7 @@ import java.util.Set; /** Controller for battery usage breakdown preference group. */ public class BatteryUsageBreakdownController extends BasePreferenceController - implements LifecycleObserver, OnResume, OnDestroy { + implements LifecycleObserver, OnResume, OnDestroy, OnCreate, OnSaveInstanceState { private static final String TAG = "BatteryUsageBreakdownController"; private static final String ROOT_PREFERENCE_KEY = "battery_usage_breakdown"; private static final String FOOTER_PREFERENCE_KEY = "battery_usage_footer"; @@ -67,6 +72,7 @@ public class BatteryUsageBreakdownController extends BasePreferenceController private static final String PACKAGE_NAME_NONE = "none"; private static final String SLOT_TIMESTAMP = "slot_timestamp"; private static final String ANOMALY_KEY = "anomaly_key"; + private static final String KEY_SPINNER_POSITION = "spinner_position"; private static final List EMPTY_ENTRY_LIST = new ArrayList<>(); private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED; @@ -78,12 +84,12 @@ public class BatteryUsageBreakdownController extends BasePreferenceController @VisibleForTesting final Map mPreferenceCache = new ArrayMap<>(); - private int mSpinnerPosition; private String mSlotInformation; + private SettingsSpinnerPreference mSpinnerPreference; + private SettingsSpinnerAdapter mSpinnerAdapter; @VisibleForTesting Context mPrefContext; @VisibleForTesting PreferenceCategory mRootPreference; - @VisibleForTesting SpinnerPreference mSpinnerPreference; @VisibleForTesting PreferenceGroup mAppListPreferenceGroup; @VisibleForTesting FooterPreference mFooterPreference; @VisibleForTesting BatteryDiffData mBatteryDiffData; @@ -92,6 +98,7 @@ public class BatteryUsageBreakdownController extends BasePreferenceController @VisibleForTesting String mPercentLessThanThresholdContentDescription; @VisibleForTesting boolean mIsHighlightSlot; @VisibleForTesting int mAnomalyKeyNumber; + @VisibleForTesting int mSpinnerPosition; @VisibleForTesting String mAnomalyEntryKey; @VisibleForTesting String mAnomalyHintString; @VisibleForTesting String mAnomalyHintPrefKey; @@ -110,6 +117,15 @@ public class BatteryUsageBreakdownController extends BasePreferenceController } } + @Override + public void onCreate(Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + mSpinnerPosition = savedInstanceState.getInt(KEY_SPINNER_POSITION, mSpinnerPosition); + Log.d(TAG, "onCreate() spinnerPosition=" + mSpinnerPosition); + } + @Override public void onResume() { final int currentUiMode = @@ -140,6 +156,15 @@ public class BatteryUsageBreakdownController extends BasePreferenceController return false; } + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + if (savedInstanceState == null) { + return; + } + savedInstanceState.putInt(KEY_SPINNER_POSITION, mSpinnerPosition); + Log.d(TAG, "onSaveInstanceState() spinnerPosition=" + mSpinnerPosition); + } + private boolean isAnomalyBatteryDiffEntry(BatteryDiffEntry entry) { return mIsHighlightSlot && mAnomalyEntryKey != null @@ -218,11 +243,14 @@ public class BatteryUsageBreakdownController extends BasePreferenceController formatPercentage); mAppListPreferenceGroup.setOrderingAsAdded(false); - mSpinnerPreference.initializeSpinner( + mSpinnerAdapter = new SettingsSpinnerAdapter<>(mPrefContext); + mSpinnerAdapter.addAll( new String[] { mPrefContext.getString(R.string.battery_usage_spinner_view_by_apps), mPrefContext.getString(R.string.battery_usage_spinner_view_by_systems) - }, + }); + mSpinnerPreference.setAdapter(mSpinnerAdapter); + mSpinnerPreference.setOnItemSelectedListener( new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected( @@ -244,6 +272,7 @@ public class BatteryUsageBreakdownController extends BasePreferenceController @Override public void onNothingSelected(AdapterView parent) {} }); + mSpinnerPreference.setSelection(mSpinnerPosition); } /** diff --git a/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreference.java b/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreference.java deleted file mode 100644 index 886d00de67e..00000000000 --- a/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreference.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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.settings.fuelgauge.batteryusage; - -import android.content.Context; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.AdapterView; -import android.widget.Spinner; - -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.R; -import com.android.settingslib.widget.SettingsSpinnerAdapter; - -/** A preference which contains a spinner. */ -public class SpinnerPreference extends Preference { - private static final String TAG = "SpinnerPreference"; - - private AdapterView.OnItemSelectedListener mOnItemSelectedListener; - - @VisibleForTesting Spinner mSpinner; - @VisibleForTesting String[] mItems; - @VisibleForTesting int mSavedSpinnerPosition; - - public SpinnerPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutResource(R.layout.preference_spinner); - } - - void initializeSpinner( - String[] items, AdapterView.OnItemSelectedListener onItemSelectedListener) { - mItems = items; - mOnItemSelectedListener = onItemSelectedListener; - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - if (mSpinner != null) { - return; - } - - mSpinner = (Spinner) view.findViewById(R.id.spinner); - mSpinner.setAdapter(new SpinnerAdapter(getContext(), mItems)); - mSpinner.setSelection(mSavedSpinnerPosition); - mSpinner.setLongClickable(false); - if (mOnItemSelectedListener != null) { - mSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - } - } - - @Override - protected Parcelable onSaveInstanceState() { - if (mSpinner == null) { - return super.onSaveInstanceState(); - } - Log.d(TAG, "onSaveInstanceState() spinnerPosition=" + mSpinner.getSelectedItemPosition()); - return new SavedState(super.onSaveInstanceState(), mSpinner.getSelectedItemPosition()); - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (state == null || state == BaseSavedState.EMPTY_STATE) { - super.onRestoreInstanceState(state); - return; - } - if (!(state instanceof SavedState)) { - // To avoid the IllegalArgumentException, return the BaseSavedState.EMPTY_STATE. - super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); - return; - } - SavedState savedState = (SavedState) state; - super.onRestoreInstanceState(savedState.getSuperState()); - mSavedSpinnerPosition = savedState.getSpinnerPosition(); - if (mOnItemSelectedListener != null) { - mOnItemSelectedListener.onItemSelected( - /* parent= */ null, - /* view= */ null, - savedState.getSpinnerPosition(), - /* id= */ 0); - } - Log.d(TAG, "onRestoreInstanceState() spinnerPosition=" + savedState.getSpinnerPosition()); - } - - @VisibleForTesting - static class SavedState extends BaseSavedState { - private int mSpinnerPosition; - - SavedState(Parcelable superState, int spinnerPosition) { - super(superState); - mSpinnerPosition = spinnerPosition; - } - - int getSpinnerPosition() { - return mSpinnerPosition; - } - } - - private static class SpinnerAdapter extends SettingsSpinnerAdapter { - private final String[] mItems; - - SpinnerAdapter(Context context, String[] items) { - super(context); - mItems = items; - } - - @Override - public int getCount() { - return mItems.length; - } - - @Override - public CharSequence getItem(int position) { - return mItems[position]; - } - } -} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java index 4c648083106..85fc6e210ce 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBreakdownControllerTest.java @@ -29,6 +29,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.LocaleList; import android.text.format.DateUtils; @@ -57,6 +58,7 @@ public final class BatteryUsageBreakdownControllerTest { private static final String PREF_KEY = "pref_key"; private static final String PREF_KEY2 = "pref_key2"; private static final String PREF_SUMMARY = "fake preference summary"; + private static final String KEY_SPINNER_POSITION = "spinner_position"; private static final long TIME_LESS_THAN_HALF_MINUTE = DateUtils.MINUTE_IN_MILLIS / 2 - 1; @Mock private InstrumentedPreferenceFragment mFragment; @@ -148,6 +150,15 @@ public final class BatteryUsageBreakdownControllerTest { verify(mAppListPreferenceGroup).removeAll(); } + @Test + public void onSaveInstanceState_returnExpectedResult() { + mBatteryUsageBreakdownController.mSpinnerPosition = 1; + final Bundle savedInstanceState = new Bundle(); + mBatteryUsageBreakdownController.onSaveInstanceState(savedInstanceState); + + assertThat(savedInstanceState.getInt(KEY_SPINNER_POSITION)).isEqualTo(1); + } + @Test public void addAllPreferences_addAllPreferences() { final String appLabel = "fake app label"; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreferenceTest.java deleted file mode 100644 index 8050984938e..00000000000 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/SpinnerPreferenceTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2022 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.settings.fuelgauge.batteryusage; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; - -import android.content.Context; -import android.widget.Spinner; - -import androidx.preference.Preference; - -import com.android.settings.R; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public final class SpinnerPreferenceTest { - - private Context mContext; - private SpinnerPreference mSpinnerPreference; - - @Mock private Spinner mMockSpinner; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - mSpinnerPreference = new SpinnerPreference(mContext, /* attrs= */ null); - } - - @Test - public void constructor_returnExpectedResult() { - assertThat(mSpinnerPreference.getLayoutResource()).isEqualTo(R.layout.preference_spinner); - } - - @Test - public void initializeSpinner_returnExpectedResult() { - final String[] items = new String[] {"item1", "item2"}; - mSpinnerPreference.initializeSpinner(items, null); - assertThat(mSpinnerPreference.mItems).isEqualTo(items); - } - - @Test - public void onSaveInstanceState_returnExpectedResult() { - doReturn(1).when(mMockSpinner).getSelectedItemPosition(); - mSpinnerPreference.mSpinner = mMockSpinner; - SpinnerPreference.SavedState savedState = - (SpinnerPreference.SavedState) mSpinnerPreference.onSaveInstanceState(); - assertThat(savedState.getSpinnerPosition()).isEqualTo(1); - } - - @Test - public void onRestoreInstanceState_returnExpectedResult() { - SpinnerPreference.SavedState savedState = - new SpinnerPreference.SavedState(Preference.BaseSavedState.EMPTY_STATE, 2); - mSpinnerPreference.onRestoreInstanceState(savedState); - assertThat(mSpinnerPreference.mSavedSpinnerPosition).isEqualTo(2); - } -} From 1a975d6f72cc89f12d0e08dd160d09cad9e7a1b5 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 16 Aug 2024 18:36:38 +0800 Subject: [PATCH 6/9] Refine WifiHotspotRepository variable names - Since the speed of hotspot design was changed to refer to allowed channels rather than usable channels, remove the "Usable" naming to avoid confusion Fix: 360312024 Flag: EXEMPT refactor Test: Manual testing atest -c WifiHotspotRepositoryTest Change-Id: Ie54a351e0f788e1b6aeec079ccf6671f7f78fb1c --- .../repository/WifiHotspotRepository.java | 48 ++++++------ .../repository/WifiHotspotRepositoryTest.java | 78 +++++++++---------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/com/android/settings/wifi/repository/WifiHotspotRepository.java b/src/com/android/settings/wifi/repository/WifiHotspotRepository.java index e523831d229..4dc2e9e52f3 100644 --- a/src/com/android/settings/wifi/repository/WifiHotspotRepository.java +++ b/src/com/android/settings/wifi/repository/WifiHotspotRepository.java @@ -397,7 +397,7 @@ public class WifiHotspotRepository { * @return {@code true} if Wi-Fi Hotspot 5 GHz Band is available */ public boolean is5gAvailable() { - if (!mBand5g.isUsableChannelsReady && is5GHzBandSupported()) { + if (!mBand5g.isChannelsReady && is5GHzBandSupported()) { isChannelAvailable(mBand5g); } return mBand5g.isAvailable(); @@ -439,7 +439,7 @@ public class WifiHotspotRepository { * @return {@code true} if Wi-Fi Hotspot 6 GHz Band is available */ public boolean is6gAvailable() { - if (!mBand6g.isUsableChannelsReady && is6GHzBandSupported()) { + if (!mBand6g.isChannelsReady && is6GHzBandSupported()) { isChannelAvailable(mBand6g); } return mBand6g.isAvailable(); @@ -475,19 +475,19 @@ public class WifiHotspotRepository { List channels = mWifiManager.getAllowedChannels(sapBand.band, OP_MODE_SAP); log("isChannelAvailable(), band:" + sapBand.band + ", channels:" + channels); - sapBand.hasUsableChannels = (channels != null && channels.size() > 0); - sapBand.isUsableChannelsUnsupported = false; + sapBand.hasChannels = (channels != null && channels.size() > 0); + sapBand.isChannelsUnsupported = false; } catch (IllegalArgumentException e) { - Log.e(TAG, "Querying usable SAP channels failed, band:" + sapBand.band); - sapBand.hasUsableChannels = false; - sapBand.isUsableChannelsUnsupported = true; + Log.e(TAG, "Querying SAP channels failed, band:" + sapBand.band); + sapBand.hasChannels = false; + sapBand.isChannelsUnsupported = true; } catch (UnsupportedOperationException e) { // This is expected on some hardware. - Log.e(TAG, "Querying usable SAP channels is unsupported, band:" + sapBand.band); - sapBand.hasUsableChannels = false; - sapBand.isUsableChannelsUnsupported = true; + Log.e(TAG, "Querying SAP channels is unsupported, band:" + sapBand.band); + sapBand.hasChannels = false; + sapBand.isChannelsUnsupported = true; } - sapBand.isUsableChannelsReady = true; + sapBand.isChannelsReady = true; log("isChannelAvailable(), " + sapBand); return sapBand.isAvailable(); } @@ -531,8 +531,8 @@ public class WifiHotspotRepository { } protected void purgeRefreshData() { - mBand5g.isUsableChannelsReady = false; - mBand6g.isUsableChannelsReady = false; + mBand5g.isChannelsReady = false; + mBand6g.isChannelsReady = false; } protected void startAutoRefresh() { @@ -615,15 +615,15 @@ public class WifiHotspotRepository { @VisibleForTesting void updateCapabilityChanged() { - if (mBand5g.isUsableChannelsUnsupported) { + if (mBand5g.isChannelsUnsupported) { update5gAvailable(); log("updateCapabilityChanged(), " + mBand5g); } - if (mBand6g.isUsableChannelsUnsupported) { + if (mBand6g.isChannelsUnsupported) { update6gAvailable(); log("updateCapabilityChanged(), " + mBand6g); } - if (mBand5g.isUsableChannelsUnsupported || mBand6g.isUsableChannelsUnsupported) { + if (mBand5g.isChannelsUnsupported || mBand6g.isChannelsUnsupported) { updateSpeedType(); } } @@ -676,9 +676,9 @@ public class WifiHotspotRepository { @VisibleForTesting static class SapBand { public int band; - public boolean isUsableChannelsReady; - public boolean hasUsableChannels; - public boolean isUsableChannelsUnsupported; + public boolean isChannelsReady; + public boolean hasChannels; + public boolean isChannelsUnsupported; public boolean hasCapability; SapBand(int band) { @@ -689,7 +689,7 @@ public class WifiHotspotRepository { * Return whether SoftAp band is available or not. */ public boolean isAvailable() { - return isUsableChannelsUnsupported ? hasCapability : hasUsableChannels; + return isChannelsUnsupported ? hasCapability : hasChannels; } @Override @@ -697,10 +697,10 @@ public class WifiHotspotRepository { public String toString() { return "SapBand{" + "band:" + band - + ",isUsableChannelsReady:" + isUsableChannelsReady - + ",hasUsableChannels:" + hasUsableChannels - + ",isUsableChannelsUnsupported:" + isUsableChannelsUnsupported - + ",hasChannelsCapability:" + hasCapability + + ",isChannelsReady:" + isChannelsReady + + ",hasChannels:" + hasChannels + + ",isChannelsUnsupported:" + isChannelsUnsupported + + ",hasCapability:" + hasCapability + '}'; } } diff --git a/tests/unit/src/com/android/settings/wifi/repository/WifiHotspotRepositoryTest.java b/tests/unit/src/com/android/settings/wifi/repository/WifiHotspotRepositoryTest.java index 3571a36bde7..4765d180119 100644 --- a/tests/unit/src/com/android/settings/wifi/repository/WifiHotspotRepositoryTest.java +++ b/tests/unit/src/com/android/settings/wifi/repository/WifiHotspotRepositoryTest.java @@ -118,12 +118,12 @@ public class WifiHotspotRepositoryTest { mRepository.mSecurityType = mSecurityType; mRepository.mSpeedType = mSpeedType; mRepository.mIsDualBand = true; - mRepository.mBand5g.isUsableChannelsReady = true; - mRepository.mBand5g.isUsableChannelsUnsupported = false; - mRepository.mBand5g.hasUsableChannels = true; - mRepository.mBand6g.isUsableChannelsReady = true; - mRepository.mBand6g.isUsableChannelsUnsupported = false; - mRepository.mBand6g.hasUsableChannels = true; + mRepository.mBand5g.isChannelsReady = true; + mRepository.mBand5g.isChannelsUnsupported = false; + mRepository.mBand5g.hasChannels = true; + mRepository.mBand6g.isChannelsReady = true; + mRepository.mBand6g.isChannelsUnsupported = false; + mRepository.mBand6g.hasChannels = true; } @Test @@ -382,7 +382,7 @@ public class WifiHotspotRepositoryTest { @Test public void updateSpeedType_singleBand5gPreferredBut5gUnavailable_get2gSpeedType() { mRepository.mIsDualBand = false; - mRepository.mBand5g.hasUsableChannels = false; + mRepository.mBand5g.hasChannels = false; SoftApConfiguration config = new SoftApConfiguration.Builder() .setBand(WIFI_5GHZ_BAND_PREFERRED).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); @@ -407,7 +407,7 @@ public class WifiHotspotRepositoryTest { @Test public void updateSpeedType_singleBand6gPreferredBut6gUnavailable_get5gSpeedType() { mRepository.mIsDualBand = false; - mRepository.mBand6g.hasUsableChannels = false; + mRepository.mBand6g.hasChannels = false; SoftApConfiguration config = new SoftApConfiguration.Builder() .setBand(WIFI_6GHZ_BAND_PREFERRED).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); @@ -420,8 +420,8 @@ public class WifiHotspotRepositoryTest { @Test public void updateSpeedType_singleBand6gPreferredBut5gAnd6gUnavailable_get2gSpeedType() { mRepository.mIsDualBand = false; - mRepository.mBand5g.hasUsableChannels = false; - mRepository.mBand6g.hasUsableChannels = false; + mRepository.mBand5g.hasChannels = false; + mRepository.mBand6g.hasChannels = false; SoftApConfiguration config = new SoftApConfiguration.Builder() .setBand(WIFI_6GHZ_BAND_PREFERRED).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); @@ -446,7 +446,7 @@ public class WifiHotspotRepositoryTest { @Test public void updateSpeedType_dualBand2gAnd5gBut5gUnavailable_get2gSpeedType() { mRepository.mIsDualBand = true; - mRepository.mBand5g.hasUsableChannels = false; + mRepository.mBand5g.hasChannels = false; SoftApConfiguration config = new SoftApConfiguration.Builder() .setBand(WIFI_5GHZ_BAND_PREFERRED).build(); when(mWifiManager.getSoftApConfiguration()).thenReturn(config); @@ -562,19 +562,19 @@ public class WifiHotspotRepositoryTest { } @Test - public void is5gAvailable_hasUsableChannels_returnTrue() { + public void is5gAvailable_hasChannels_returnTrue() { mRepository.mIs5gBandSupported = true; // Reset m5gBand to trigger an update - mRepository.mBand5g.isUsableChannelsReady = false; + mRepository.mBand5g.isChannelsReady = false; assertThat(mRepository.is5gAvailable()).isTrue(); } @Test - public void is5gAvailable_noUsableChannels_returnFalse() { + public void is5gAvailable_noChannels_returnFalse() { mRepository.mIs5gBandSupported = true; // Reset m5gBand to trigger an update - mRepository.mBand5g.isUsableChannelsReady = false; + mRepository.mBand5g.isChannelsReady = false; when(mWifiManager.getAllowedChannels(WifiScanner.WIFI_BAND_5_GHZ_WITH_DFS, OP_MODE_SAP)) .thenReturn(null); @@ -585,7 +585,7 @@ public class WifiHotspotRepositoryTest { @UiThreadTest public void get5gAvailable_shouldNotReturnNull() { // Reset m5gBand to trigger an update - mRepository.mBand5g.isUsableChannelsReady = false; + mRepository.mBand5g.isChannelsReady = false; assertThat(mRepository.get5gAvailable()).isNotNull(); } @@ -606,19 +606,19 @@ public class WifiHotspotRepositoryTest { } @Test - public void is6gAvailable_hasUsableChannels_returnTrue() { + public void is6gAvailable_hasChannels_returnTrue() { mRepository.mIs6gBandSupported = true; // Reset m6gBand to trigger an update - mRepository.mBand6g.isUsableChannelsReady = false; + mRepository.mBand6g.isChannelsReady = false; assertThat(mRepository.is6gAvailable()).isTrue(); } @Test - public void is6gAvailable_noUsableChannels_returnFalse() { + public void is6gAvailable_noChannels_returnFalse() { mRepository.mIs6gBandSupported = true; // Reset m6gBand to trigger an update - mRepository.mBand6g.isUsableChannelsReady = false; + mRepository.mBand6g.isChannelsReady = false; when(mWifiManager.getAllowedChannels(WifiScanner.WIFI_BAND_6_GHZ, OP_MODE_SAP)) .thenReturn(null); @@ -658,33 +658,33 @@ public class WifiHotspotRepositoryTest { } @Test - public void isChannelAvailable_throwIllegalArgumentException_hasUsableChannelsFalse() { + public void isChannelAvailable_throwIllegalArgumentException_hasChannelsFalse() { doThrow(IllegalArgumentException.class).when(mWifiManager) .getAllowedChannels(WifiScanner.WIFI_BAND_6_GHZ, OP_MODE_SAP); mRepository.isChannelAvailable(mRepository.mBand6g); - assertThat(mRepository.mBand6g.hasUsableChannels).isFalse(); - assertThat(mRepository.mBand6g.isUsableChannelsUnsupported).isTrue(); + assertThat(mRepository.mBand6g.hasChannels).isFalse(); + assertThat(mRepository.mBand6g.isChannelsUnsupported).isTrue(); } @Test - public void isChannelAvailable_throwUnsupportedOperationException_hasUsableChannelsFalse() { + public void isChannelAvailable_throwUnsupportedOperationException_hasChannelsFalse() { doThrow(UnsupportedOperationException.class).when(mWifiManager) .getAllowedChannels(WifiScanner.WIFI_BAND_6_GHZ, OP_MODE_SAP); mRepository.isChannelAvailable(mRepository.mBand6g); - assertThat(mRepository.mBand6g.hasUsableChannels).isFalse(); - assertThat(mRepository.mBand6g.isUsableChannelsUnsupported).isTrue(); + assertThat(mRepository.mBand6g.hasChannels).isFalse(); + assertThat(mRepository.mBand6g.isChannelsUnsupported).isTrue(); } @Test - public void isChannelAvailable_noExceptionAndHasUsableChannels_hasUsableChannelsTrue() { + public void isChannelAvailable_noExceptionAndHasChannels_hasChannelsTrue() { mRepository.isChannelAvailable(mRepository.mBand6g); - assertThat(mRepository.mBand6g.hasUsableChannels).isTrue(); - assertThat(mRepository.mBand6g.isUsableChannelsUnsupported).isFalse(); + assertThat(mRepository.mBand6g.hasChannels).isTrue(); + assertThat(mRepository.mBand6g.isChannelsUnsupported).isFalse(); } @Test @@ -744,9 +744,9 @@ public class WifiHotspotRepositoryTest { } @Test - public void updateCapabilityChanged_band5gUsableChannelsUnsupported_update5gAvailable() { + public void updateCapabilityChanged_band5gChannelsUnsupported_update5gAvailable() { mRepository = spy(new WifiHotspotRepository(mContext, mWifiManager, mTetheringManager)); - mRepository.mBand5g.isUsableChannelsUnsupported = true; + mRepository.mBand5g.isChannelsUnsupported = true; mRepository.updateCapabilityChanged(); @@ -755,9 +755,9 @@ public class WifiHotspotRepositoryTest { } @Test - public void updateCapabilityChanged_band6gUsableChannelsUnsupported_update5gAvailable() { + public void updateCapabilityChanged_band6gChannelsUnsupported_update5gAvailable() { mRepository = spy(new WifiHotspotRepository(mContext, mWifiManager, mTetheringManager)); - mRepository.mBand6g.isUsableChannelsUnsupported = true; + mRepository.mBand6g.isChannelsUnsupported = true; mRepository.updateCapabilityChanged(); @@ -766,18 +766,18 @@ public class WifiHotspotRepositoryTest { } @Test - public void isAvailable_isUsableChannelsUnsupportedFalse_returnHasUsableChannels() { - mRepository.mBand6g.isUsableChannelsUnsupported = false; - mRepository.mBand6g.hasUsableChannels = false; + public void isAvailable_isChannelsUnsupportedFalse_returnHasChannels() { + mRepository.mBand6g.isChannelsUnsupported = false; + mRepository.mBand6g.hasChannels = false; mRepository.mBand6g.hasCapability = true; assertThat(mRepository.mBand6g.isAvailable()).isFalse(); } @Test - public void isAvailable_isUsableChannelsUnsupportedTrue_returnHasCapability() { - mRepository.mBand6g.isUsableChannelsUnsupported = true; - mRepository.mBand6g.hasUsableChannels = false; + public void isAvailable_isChannelsUnsupportedTrue_returnHasCapability() { + mRepository.mBand6g.isChannelsUnsupported = true; + mRepository.mBand6g.hasChannels = false; mRepository.mBand6g.hasCapability = true; assertThat(mRepository.mBand6g.isAvailable()).isTrue(); From b676ca3a62878fdcd01ac7c55fc67621a2ce9f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Fri, 16 Aug 2024 15:15:00 +0200 Subject: [PATCH 7/9] Fix several issues related to CircularIconsPreference * Sometimes would cause an unending stream of accessibility events (particularly if starting off-screen). This would break TalkBack and anything that depends on UiAutomator (although the system itself took it like a champ). * Sometimes would not load images (because the ViewTreeObserver would never fire onGlobalLayout after being added because measured width was 0, even though a new width was calculated later). * Would not recalculate the number of icons that fit if the width changes after the first layout. Combining ViewHolders with waiting for measuring and/or ViewTreeObservers was always a wonky approach, even though it should've worked in theory. This should be more robust. Also fixes the unwanted animation on mode screen load related to the absence of the header name being applied a tad too late. Fixes: 359948417 Fixes: 360072876 Fixes: 360328804 Test: atest SettingsRoboTests + manual + adb shell uiautomator events Flag: android.app.modes_ui Change-Id: I7e5dfbdab220d1ebc1c68e5e87ce544ee86b6a65 --- res/layout/preference_circular_icons.xml | 4 +- .../AbstractZenModeHeaderController.java | 15 +- .../modes/CircularIconsPreference.java | 202 ++------------- .../notification/modes/CircularIconsView.java | 232 ++++++++++++++++++ .../settings/notification/modes/IconUtil.java | 20 +- .../ZenModeAppsLinkPreferenceController.java | 4 +- .../ZenModeOtherLinkPreferenceController.java | 2 +- ...ZenModePeopleLinkPreferenceController.java | 2 +- .../modes/CircularIconsPreferenceTest.java | 182 ++++++++------ .../TestableCircularIconsPreference.java | 4 +- ...nModeAppsLinkPreferenceControllerTest.java | 17 +- ...ModeOtherLinkPreferenceControllerTest.java | 4 +- ...odePeopleLinkPreferenceControllerTest.java | 25 +- 13 files changed, 414 insertions(+), 299 deletions(-) create mode 100644 src/com/android/settings/notification/modes/CircularIconsView.java diff --git a/res/layout/preference_circular_icons.xml b/res/layout/preference_circular_icons.xml index 863d2288513..e1d7cfeb470 100644 --- a/res/layout/preference_circular_icons.xml +++ b/res/layout/preference_circular_icons.xml @@ -58,8 +58,8 @@ android:lineBreakWordStyle="phrase" android:maxLines="10"/> - - + icons, int extraItems) { - static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0); - } - - private Executor mUiExecutor; - - // Chronologically, fields will be set top-to-bottom. - @Nullable private CircularIconSet mIconSet; - @Nullable private ListenableFuture> mPendingLoadIconsFuture; - @Nullable private LoadedIcons mLoadedIcons; + private CircularIconSet mIconSet = CircularIconSet.EMPTY; public CircularIconsPreference(Context context) { super(context); - init(context); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - public CircularIconsPreference(Context context, Executor uiExecutor) { - this(context); - mUiExecutor = uiExecutor; + init(); } public CircularIconsPreference(Context context, AttributeSet attrs) { super(context, attrs); - init(context); + init(); } public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(context); + init(); } public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - init(context); + init(); } - private void init(Context context) { - mUiExecutor = context.getMainExecutor(); + private void init() { setLayoutResource(R.layout.preference_circular_icons); } - void displayIcons(CircularIconSet iconSet) { - displayIcons(iconSet, null); + void setIcons(CircularIconSet iconSet) { + setIcons(iconSet, null); } - void displayIcons(CircularIconSet iconSet, @Nullable Equivalence itemEquivalence) { - if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) { + void setIcons(CircularIconSet iconSet, @Nullable Equivalence itemEquivalence) { + if (mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) { return; } + mIconSet = iconSet; - - mLoadedIcons = null; - if (mPendingLoadIconsFuture != null) { - mPendingLoadIconsFuture.cancel(true); - mPendingLoadIconsFuture = null; - } - notifyChanged(); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); + CircularIconsView iconContainer = checkNotNull( + (CircularIconsView) holder.findViewById(R.id.circles_container)); - LinearLayout iconContainer = checkNotNull( - (LinearLayout) holder.findViewById(R.id.circles_container)); - bindIconContainer(iconContainer); - } - - private void bindIconContainer(LinearLayout container) { - if (mLoadedIcons != null) { - // We have the icons ready to display already, show them. - setDrawables(container, mLoadedIcons); - } else if (mIconSet != null) { - // We know what icons we want, but haven't yet loaded them. - if (mIconSet.size() == 0) { - container.setVisibility(View.GONE); - mLoadedIcons = LoadedIcons.EMPTY; - return; - } - container.setVisibility(View.VISIBLE); - if (container.getMeasuredWidth() != 0) { - startLoadingIcons(container, mIconSet); - } else { - container.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - container.getViewTreeObserver().removeOnGlobalLayoutListener(this); - notifyChanged(); - } - } - ); - } - } - } - - private void startLoadingIcons(LinearLayout container, CircularIconSet iconSet) { - Resources res = getContext().getResources(); - int availableSpace = container.getMeasuredWidth(); - int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) - + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); - int numIconsThatFit = availableSpace / iconHorizontalSpace; - - List> iconFutures; - int extraItems; - if (iconSet.size() > numIconsThatFit) { - // Reserve one space for the (+xx) textview. - int numIconsToShow = numIconsThatFit - 1; - if (numIconsToShow < 0) { - numIconsToShow = 0; - } - iconFutures = iconSet.getIcons(numIconsToShow); - extraItems = iconSet.size() - numIconsToShow; - } else { - // Fit exactly or with remaining space. - iconFutures = iconSet.getIcons(); - extraItems = 0; - } - - // Display icons when all are ready (more consistent than randomly loading). - mPendingLoadIconsFuture = Futures.allAsList(iconFutures); - FutureUtil.whenDone( - mPendingLoadIconsFuture, - icons -> { - mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems); - notifyChanged(); // So that view is rebound and icons actually shown. - }, - mUiExecutor); - } - - private void setDrawables(LinearLayout container, LoadedIcons loadedIcons) { - // Rearrange child views until we have ImageViews... - LayoutInflater inflater = LayoutInflater.from(getContext()); - int numImages = loadedIcons.icons.size(); - int numImageViews = getChildCount(container, ImageView.class); - if (numImages > numImageViews) { - for (int i = 0; i < numImages - numImageViews; i++) { - ImageView imageView = (ImageView) inflater.inflate( - R.layout.preference_circular_icons_item, container, false); - container.addView(imageView, 0); - } - } else if (numImageViews > numImages) { - for (int i = 0; i < numImageViews - numImages; i++) { - container.removeViewAt(0); - } - } - // ... plus 0/1 TextViews at the end. - if (loadedIcons.extraItems > 0 && !(getLastChild(container) instanceof TextView)) { - TextView plusView = (TextView) inflater.inflate( - R.layout.preference_circular_icons_plus_item, container, false); - container.addView(plusView); - } else if (loadedIcons.extraItems == 0 && (getLastChild(container) instanceof TextView)) { - container.removeViewAt(container.getChildCount() - 1); - } - - // Show images (and +n if needed). - for (int i = 0; i < numImages; i++) { - ImageView imageView = (ImageView) container.getChildAt(i); - imageView.setImageDrawable(loadedIcons.icons.get(i)); - } - if (loadedIcons.extraItems > 0) { - TextView textView = (TextView) checkNotNull(getLastChild(container)); - textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, - loadedIcons.extraItems)); - } - - // Apply enabled/disabled style. - for (int i = 0; i < container.getChildCount(); i++) { - View child = container.getChildAt(i); - child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA); - } - } - - private static int getChildCount(ViewGroup parent, Class childClass) { - int count = 0; - for (int i = 0; i < parent.getChildCount(); i++) { - if (childClass.isInstance(parent.getChildAt(i))) { - count++; - } - } - return count; - } - - @Nullable - private static View getLastChild(ViewGroup parent) { - if (parent.getChildCount() == 0) { - return null; - } - return parent.getChildAt(parent.getChildCount() - 1); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @Nullable - LoadedIcons getLoadedIcons() { - return mLoadedIcons; + iconContainer.setVisibility(mIconSet != null && mIconSet.size() == 0 ? GONE : VISIBLE); + iconContainer.setEnabled(isEnabled()); + iconContainer.setIcons(mIconSet); } } diff --git a/src/com/android/settings/notification/modes/CircularIconsView.java b/src/com/android/settings/notification/modes/CircularIconsView.java new file mode 100644 index 00000000000..b0e4280129b --- /dev/null +++ b/src/com/android/settings/notification/modes/CircularIconsView.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2024 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.settings.notification.modes; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.concurrent.Executor; + +public class CircularIconsView extends LinearLayout { + + private static final float DISABLED_ITEM_ALPHA = 0.3f; + + record Icons(ImmutableList icons, int extraItems) { } + + private Executor mUiExecutor; + private int mNumberOfCirclesThatFit; + + // Chronologically, fields will be set top-to-bottom. + @Nullable private CircularIconSet mIconSet; + @Nullable private ListenableFuture> mPendingLoadIconsFuture; + @Nullable private Icons mDisplayedIcons; + + public CircularIconsView(Context context) { + super(context); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs) { + super(context, attrs); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setUiExecutor(context.getMainExecutor()); + } + + @VisibleForTesting + void setUiExecutor(Executor uiExecutor) { + mUiExecutor = uiExecutor; + } + + void setIcons(CircularIconSet iconSet) { + if (mIconSet != null && mIconSet.equals(iconSet)) { + return; + } + + mIconSet = checkNotNull(iconSet); + cancelPendingTasks(); + if (getMeasuredWidth() != 0) { + startLoadingIcons(iconSet); + } + } + + private void cancelPendingTasks() { + mDisplayedIcons = null; + if (mPendingLoadIconsFuture != null) { + mPendingLoadIconsFuture.cancel(true); + mPendingLoadIconsFuture = null; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + int numFitting = getNumberOfCirclesThatFit(); + if (mNumberOfCirclesThatFit != numFitting) { + // View has been measured for the first time OR its dimensions have changed since then. + // Keep track, because we want to reload stuff if more (or less) items fit. + mNumberOfCirclesThatFit = numFitting; + + if (mIconSet != null) { + cancelPendingTasks(); + startLoadingIcons(mIconSet); + } + } + } + + private int getNumberOfCirclesThatFit() { + Resources res = getContext().getResources(); + int availableSpace = getMeasuredWidth(); + int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); + return availableSpace / iconHorizontalSpace; + } + + private void startLoadingIcons(CircularIconSet iconSet) { + int numCirclesThatFit = getNumberOfCirclesThatFit(); + + List> iconFutures; + int extraItems; + if (iconSet.size() > numCirclesThatFit) { + // Reserve one space for the (+xx) textview. + int numIconsToShow = numCirclesThatFit - 1; + if (numIconsToShow < 0) { + numIconsToShow = 0; + } + iconFutures = iconSet.getIcons(numIconsToShow); + extraItems = iconSet.size() - numIconsToShow; + } else { + // Fit exactly or with remaining space. + iconFutures = iconSet.getIcons(); + extraItems = 0; + } + + // Display icons when all are ready (more consistent than randomly loading). + mPendingLoadIconsFuture = Futures.allAsList(iconFutures); + FutureUtil.whenDone( + mPendingLoadIconsFuture, + icons -> setDrawables(new Icons(ImmutableList.copyOf(icons), extraItems)), + mUiExecutor); + } + + private void setDrawables(Icons icons) { + mDisplayedIcons = icons; + + // Rearrange child views until we have ImageViews... + LayoutInflater inflater = LayoutInflater.from(getContext()); + int numImages = icons.icons.size(); + int numImageViews = getChildCount(ImageView.class); + if (numImages > numImageViews) { + for (int i = 0; i < numImages - numImageViews; i++) { + ImageView imageView = (ImageView) inflater.inflate( + R.layout.preference_circular_icons_item, this, false); + addView(imageView, 0); + } + } else if (numImageViews > numImages) { + for (int i = 0; i < numImageViews - numImages; i++) { + removeViewAt(0); + } + } + // ... plus 0/1 TextViews at the end. + if (icons.extraItems > 0 && !(getLastChild() instanceof TextView)) { + TextView plusView = (TextView) inflater.inflate( + R.layout.preference_circular_icons_plus_item, this, false); + this.addView(plusView); + } else if (icons.extraItems == 0 && (getLastChild() instanceof TextView)) { + removeViewAt(getChildCount() - 1); + } + + // Show images (and +n if needed). + for (int i = 0; i < numImages; i++) { + ImageView imageView = (ImageView) getChildAt(i); + imageView.setImageDrawable(icons.icons.get(i)); + } + if (icons.extraItems > 0) { + TextView textView = (TextView) checkNotNull(getLastChild()); + textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, + icons.extraItems)); + } + + applyEnabledDisabledAppearance(isEnabled()); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + applyEnabledDisabledAppearance(isEnabled()); + } + + private void applyEnabledDisabledAppearance(boolean enabled) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.setAlpha(enabled ? 1.0f : DISABLED_ITEM_ALPHA); + } + } + + private int getChildCount(Class childClass) { + int count = 0; + for (int i = 0; i < getChildCount(); i++) { + if (childClass.isInstance(getChildAt(i))) { + count++; + } + } + return count; + } + + @Nullable + private View getLastChild() { + if (getChildCount() == 0) { + return null; + } + return getChildAt(getChildCount() - 1); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Nullable + Icons getDisplayedIcons() { + return mDisplayedIcons; + } +} diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index dc4d875869e..33d0d961698 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -79,6 +79,7 @@ class IconUtil { @Px int innerSizePx = res.getDimensionPixelSize(R.dimen.zen_mode_header_inner_icon_size); Drawable base = composeIcons( + context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), @@ -89,6 +90,7 @@ class IconUtil { innerSizePx); Drawable selected = composeIcons( + context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary), outerSizePx, @@ -111,6 +113,7 @@ class IconUtil { */ static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), context.getResources().getDimensionPixelSize( @@ -129,6 +132,7 @@ class IconUtil { */ static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) { return composeIconCircle( + context.getResources(), context.getColorStateList(R.color.modes_icon_selectable_background), context.getResources().getDimensionPixelSize( R.dimen.zen_mode_icon_list_item_circle_diameter), @@ -146,6 +150,7 @@ class IconUtil { static Drawable makeCircularIconPreferenceItem(@NonNull Context context, @DrawableRes int iconResId) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), context.getResources().getDimensionPixelSize( @@ -166,6 +171,7 @@ class IconUtil { Resources res = context.getResources(); if (Strings.isNullOrEmpty(displayName)) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorTertiaryContainer), res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter), @@ -204,17 +210,17 @@ class IconUtil { return new BitmapDrawable(context.getResources(), bitmap); } - private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, - Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { - return composeIcons(new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, icon, - iconColor, iconSizePx); + private static Drawable composeIconCircle(Resources res, ColorStateList circleColor, + @Px int circleDiameterPx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { + return composeIcons(res, new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, + icon, iconColor, iconSizePx); } - private static Drawable composeIcons(Drawable outer, ColorStateList outerColor, + private static Drawable composeIcons(Resources res, Drawable outer, ColorStateList outerColor, @Px int outerSizePx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { - Drawable background = checkNotNull(outer.getConstantState()).newDrawable().mutate(); + Drawable background = checkNotNull(outer.getConstantState()).newDrawable(res).mutate(); background.setTintList(outerColor); - Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable().mutate(); + Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable(res).mutate(); foreground.setTintList(iconColor); LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground }); diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java index a3cb30d5657..3f95028f0c4 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java @@ -109,7 +109,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) { mPreference.setSummary(R.string.zen_mode_apps_none_apps); - mPreference.displayIcons(CircularIconSet.EMPTY); + mPreference.setIcons(CircularIconSet.EMPTY); if (mAppSession != null) { mAppSession.deactivateSession(); } @@ -151,7 +151,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr ImmutableList apps = getAppsBypassingDndSortedByName(allApps); mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps)); - mPreference.displayIcons(new CircularIconSet<>(apps, + mPreference.setIcons(new CircularIconSet<>(apps, app -> mAppIconRetriever.apply(app.info)), APP_ENTRY_EQUIVALENCE); } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index 15e0edcf1df..939c7a605c9 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -72,7 +72,7 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)); - ((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy())); + ((CircularIconsPreference) preference).setIcons(getSoundIcons(zenMode.getPolicy())); } private CircularIconSet getSoundIcons(ZenPolicy policy) { diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index 4610c35ca82..08a551e9bbb 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -95,7 +95,7 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy())); - ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()), + ((CircularIconsPreference) preference).setIcons(getPeopleIcons(zenMode.getPolicy()), PEOPLE_ITEM_EQUIVALENCE); } diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java index d145f255c5b..55448329d6f 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java @@ -62,8 +62,7 @@ public class CircularIconsPreferenceTest { private Context mContext; private CircularIconsPreference mPreference; - private PreferenceViewHolder mViewHolder; - private ViewGroup mContainer; + private CircularIconsView mContainer; private int mOneIconWidth; @@ -73,179 +72,211 @@ public class CircularIconsPreferenceTest { mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); mPreference = new TestableCircularIconsPreference(mContext); - // Tests should call bindAndMeasureViewHolder() so that icons can be added. + // Tests should call bindAndLayoutViewHolder() so that icons can be added. Resources res = mContext.getResources(); mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); } - private void bindAndMeasureViewHolder(int viewWidth) { + private void bindAndLayoutViewHolder(int viewWidth) { bindViewHolder(); - measureViewHolder(viewWidth); + layoutViewHolder(viewWidth); } private void bindViewHolder() { View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); mContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container)); - mViewHolder = PreferenceViewHolder.createInstanceForTests(preferenceView); - mPreference.onBindViewHolder(mViewHolder); + mContainer.setUiExecutor(MoreExecutors.directExecutor()); + PreferenceViewHolder viewHolder = PreferenceViewHolder.createInstanceForTests( + preferenceView); + mPreference.onBindViewHolder(viewHolder); } - private void measureViewHolder(int viewWidth) { + private void layoutViewHolder(int viewWidth) { checkState(mContainer != null, "Call bindViewHolder() first!"); mContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY), makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); - mContainer.getViewTreeObserver().dispatchOnGlobalLayout(); + mContainer.layout(0, 0, viewWidth, 1000); } @Test - public void displayIcons_loadsIcons() { + public void setIcons_loadsIcons() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).hasSize(2); - assertThat(((ColorDrawable) getIcons(mContainer).get(0)).getColor()).isEqualTo(1); - assertThat(((ColorDrawable) getIcons(mContainer).get(1)).getColor()).isEqualTo(2); + assertThat(getDrawables(mContainer)).hasSize(2); + assertThat(((ColorDrawable) getDrawables(mContainer).get(0)).getColor()).isEqualTo(1); + assertThat(((ColorDrawable) getDrawables(mContainer).get(1)).getColor()).isEqualTo(2); assertThat(getPlusText(mContainer)).isNull(); } @Test - public void displayIcons_noIcons_hidesRow() { + public void setIcons_noIcons_hidesRow() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(mContainer.getVisibility()).isEqualTo(View.GONE); } @Test - public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { + public void setIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { int width = 300; int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( IntStream.range(0, fittingCircles).boxed().toList(), ColorDrawable::new); - bindAndMeasureViewHolder(width); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(width); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).hasSize(fittingCircles); - assertThat(getIcons(mContainer)).containsExactlyElementsIn( + assertThat(getDrawables(mContainer)).hasSize(fittingCircles); + assertThat(getDrawables(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons()).get()).inOrder(); assertThat(getPlusText(mContainer)).isNull(); - } @Test - public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { + public void setIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { int width = 300; int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( IntStream.range(0, fittingCircles + 5).boxed().toList(), ColorDrawable::new); - bindAndMeasureViewHolder(width); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(width); + mPreference.setIcons(iconSet); // N-1 icons, plus (+6) text. - assertThat(getIcons(mContainer)).hasSize(fittingCircles - 1); - assertThat(getIcons(mContainer)).containsExactlyElementsIn( + assertThat(getDrawables(mContainer)).hasSize(fittingCircles - 1); + assertThat(getDrawables(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons(fittingCircles - 1)).get()) .inOrder(); assertThat(getPlusText(mContainer)).isEqualTo("+6"); } @Test - public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() { + public void setIcons_teenyTinySpace_showsPlusIcon_noCrash() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(1); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(1); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).isEmpty(); + assertThat(getDrawables(mContainer)).isEmpty(); assertThat(getPlusText(mContainer)).isEqualTo("+2"); } @Test - public void displayIcons_beforeBind_loadsIconsOnBindAndMeasure() { + public void setIcons_beforeBind_loadsIconsOnBindAndMeasure() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); - mPreference.displayIcons(iconSet); - assertThat(mPreference.getLoadedIcons()).isNull(); // Hold... + mPreference.setIcons(iconSet); + assertThat(mContainer).isNull(); // Hold... bindViewHolder(); - assertThat(mPreference.getLoadedIcons()).isNull(); // Hooooold... + assertThat(getDrawables(mContainer)).hasSize(0); // Hooooold... - measureViewHolder(VIEW_WIDTH); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); - assertThat(getIcons(mContainer)).hasSize(3); + layoutViewHolder(VIEW_WIDTH); + assertThat(getDrawables(mContainer)).hasSize(3); } @Test - public void displayIcons_beforeMeasure_loadsIconsOnMeasure() { + public void setIcons_beforeMeasure_loadsIconsOnMeasure() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); bindViewHolder(); - mPreference.displayIcons(iconSet); - assertThat(mPreference.getLoadedIcons()).isNull(); + mPreference.setIcons(iconSet); + assertThat(getDrawables(mContainer)).hasSize(0); - measureViewHolder(VIEW_WIDTH); - assertThat(getIcons(mContainer)).hasSize(3); + layoutViewHolder(VIEW_WIDTH); + assertThat(getDrawables(mContainer)).hasSize(3); } @Test - public void displayIcons_calledAgain_reloadsIcons() { + public void setIcons_calledAgain_reloadsIcons() { CircularIconSet threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); CircularIconSet twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); CircularIconSet fourIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); + bindAndLayoutViewHolder(VIEW_WIDTH); - mPreference.displayIcons(threeIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(3); + mPreference.setIcons(threeIcons); + assertThat(getDrawables(mContainer)).hasSize(3); - mPreference.displayIcons(twoIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(2); + mPreference.setIcons(twoIcons); + assertThat(getDrawables(mContainer)).hasSize(2); - mPreference.displayIcons(fourIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(4); + mPreference.setIcons(fourIcons); + assertThat(getDrawables(mContainer)).hasSize(4); } @Test - public void displayIcons_sameSet_doesNotReloadIcons() { + public void setIcons_sameSet_doesNotReloadIcons() { CircularIconSet one = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); CircularIconSet same = Mockito.spy(new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new)); when(same.getIcons()).thenThrow(new RuntimeException("Shouldn't be called!")); - bindAndMeasureViewHolder(VIEW_WIDTH); + bindAndLayoutViewHolder(VIEW_WIDTH); - mPreference.displayIcons(one); + mPreference.setIcons(one); - mPreference.displayIcons(same); // if no exception, wasn't called. + mPreference.setIcons(same); // if no exception, wasn't called. } + @Test + public void sizeChanged_reloadsIconsIfDifferentFit() { + CircularIconSet largeIconSet = new CircularIconSet<>( + IntStream.range(0, 100).boxed().toList(), + ColorDrawable::new); + mPreference.setIcons(largeIconSet); + + // Base space -> some icons + int firstWidth = 600; + int firstFittingCircles = firstWidth / mOneIconWidth; + bindAndLayoutViewHolder(firstWidth); + + assertThat(getDrawables(mContainer)).hasSize(firstFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (firstFittingCircles - 1))); + + // More space -> more icons + int secondWidth = 1000; + int secondFittingCircles = secondWidth / mOneIconWidth; + assertThat(secondFittingCircles).isGreaterThan(firstFittingCircles); + bindAndLayoutViewHolder(secondWidth); + + assertThat(getDrawables(mContainer)).hasSize(secondFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (secondFittingCircles - 1))); + + // Less space -> fewer icons + int thirdWidth = 600; + int thirdFittingCircles = thirdWidth / mOneIconWidth; + bindAndLayoutViewHolder(thirdWidth); + + assertThat(getDrawables(mContainer)).hasSize(thirdFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (thirdFittingCircles - 1))); + } + + @Test public void onBindViewHolder_withDifferentView_reloadsIconsCorrectly() { View preferenceViewOne = LayoutInflater.from(mContext).inflate( mPreference.getLayoutResource(), null); - ViewGroup containerOne = preferenceViewOne.findViewById(R.id.circles_container); + CircularIconsView containerOne = preferenceViewOne.findViewById(R.id.circles_container); + containerOne.setUiExecutor(MoreExecutors.directExecutor()); PreferenceViewHolder viewHolderOne = PreferenceViewHolder.createInstanceForTests( preferenceViewOne); containerOne.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), @@ -253,7 +284,8 @@ public class CircularIconsPreferenceTest { View preferenceViewTwo = LayoutInflater.from(mContext).inflate( mPreference.getLayoutResource(), null); - ViewGroup containerTwo = preferenceViewTwo.findViewById(R.id.circles_container); + CircularIconsView containerTwo = preferenceViewTwo.findViewById(R.id.circles_container); + containerTwo.setUiExecutor(MoreExecutors.directExecutor()); PreferenceViewHolder viewHolderTwo = PreferenceViewHolder.createInstanceForTests( preferenceViewTwo); containerTwo.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), @@ -265,25 +297,25 @@ public class CircularIconsPreferenceTest { ColorDrawable::new); mPreference.onBindViewHolder(viewHolderOne); - mPreference.displayIcons(iconSetOne); - assertThat(getIcons(containerOne)).hasSize(3); + mPreference.setIcons(iconSetOne); + assertThat(getDrawables(containerOne)).hasSize(3); mPreference.onBindViewHolder(viewHolderTwo); - assertThat(getIcons(containerTwo)).hasSize(3); + assertThat(getDrawables(containerTwo)).hasSize(3); - mPreference.displayIcons(iconSetTwo); + mPreference.setIcons(iconSetTwo); // The second view is updated and the first view is unaffected. - assertThat(getIcons(containerTwo)).hasSize(2); - assertThat(getIcons(containerOne)).hasSize(3); + assertThat(getDrawables(containerTwo)).hasSize(2); + assertThat(getDrawables(containerOne)).hasSize(3); } @Test - public void setEnabled_afterDisplayIcons_showsEnabledOrDisabledImages() { + public void setEnabled_afterSetIcons_showsEnabledOrDisabledImages() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(getViews(mContainer)).hasSize(2); mPreference.setEnabled(false); @@ -294,13 +326,13 @@ public class CircularIconsPreferenceTest { } @Test - public void setEnabled_beforeDisplayIcons_showsEnabledOrDisabledImages() { + public void setEnabled_beforeSetIcons_showsEnabledOrDisabledImages() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); mPreference.setEnabled(false); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(getViews(mContainer)).hasSize(2); assertThat(getViews(mContainer).get(0).getAlpha()).isLessThan(1f); @@ -314,7 +346,7 @@ public class CircularIconsPreferenceTest { return views; } - private static List getIcons(ViewGroup container) { + private static List getDrawables(ViewGroup container) { ArrayList drawables = new ArrayList<>(); for (int i = 0; i < container.getChildCount(); i++) { if (container.getChildAt(i) instanceof ImageView imageView) { diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java index 6fefcacad21..6c1b0597779 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java +++ b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java @@ -20,14 +20,12 @@ import android.content.Context; import androidx.preference.PreferenceViewHolder; -import com.google.common.util.concurrent.MoreExecutors; - class TestableCircularIconsPreference extends CircularIconsPreference { private PreferenceViewHolder mLastViewHolder; TestableCircularIconsPreference(Context context) { - super(context, MoreExecutors.directExecutor()); + super(context); } @Override diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java index f656bad0f01..f77dc3ebfa0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -19,6 +19,7 @@ package com.android.settings.notification.modes; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -51,6 +52,7 @@ import android.view.View; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; +import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -81,6 +83,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { private ZenModeAppsLinkPreferenceController mController; private CircularIconsPreference mPreference; + private CircularIconsView mIconsView; private Context mContext; @Mock @@ -113,6 +116,8 @@ public final class ZenModeAppsLinkPreferenceControllerTest { // Ensure the preference view is bound & measured (needed to add child ImageViews). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); + mIconsView = checkNotNull(preferenceView.findViewById(R.id.circles_container)); + mIconsView.setUiExecutor(MoreExecutors.directExecutor()); preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); @@ -272,7 +277,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { appEntries.add(createAppEntry("test2", mContext.getUserId())); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(2); } @Test @@ -312,7 +317,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { mController.updateState(mPreference, zenModeWithNone); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); verifyNoMoreInteractions(mApplicationsState); verifyNoMoreInteractions(mSession); @@ -321,7 +326,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { verify(mApplicationsState).newSession(any(), any()); verify(mSession).rebuild(any(), any(), anyBoolean()); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); } @Test @@ -342,11 +347,11 @@ public final class ZenModeAppsLinkPreferenceControllerTest { verify(mApplicationsState).newSession(any(), any()); verify(mSession).rebuild(any(), any(), anyBoolean()); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); mController.updateState(mPreference, zenModeWithNone); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); verify(mSession).deactivateSession(); verifyNoMoreInteractions(mSession); verifyNoMoreInteractions(mApplicationsState); @@ -355,7 +360,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { // updateState()) is ignored. mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java index 8aa87e6c903..3db70fa67bc 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -95,7 +95,7 @@ public final class ZenModeOtherLinkPreferenceControllerTest { mController.updateState(pref, mode); - verify(pref).displayIcons(argThat(iconSet -> iconSet.size() == 3)); + verify(pref).setIcons(argThat(iconSet -> iconSet.size() == 3)); } @Test @@ -107,7 +107,7 @@ public final class ZenModeOtherLinkPreferenceControllerTest { mController.updateState(pref, mode); - verify(pref).displayIcons(argThat(iconSet -> + verify(pref).setIcons(argThat(iconSet -> iconSet.size() == ZenModeSummaryHelper.OTHER_SOUND_CATEGORIES.size())); } } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java index a4d141e9ca3..8555d710fa0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -22,6 +22,7 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -50,6 +51,7 @@ import android.view.View; import androidx.preference.PreferenceViewHolder; +import com.android.settings.R; import com.android.settings.notification.modes.ZenHelperBackend.Contact; import com.android.settingslib.notification.ConversationIconFactory; import com.android.settingslib.notification.modes.TestModeBuilder; @@ -76,6 +78,7 @@ public final class ZenModePeopleLinkPreferenceControllerTest { private ZenModePeopleLinkPreferenceController mController; private CircularIconsPreference mPreference; + private CircularIconsView mIconsView; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -94,6 +97,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { // Ensure the preference view is bound & measured (needed to add icons). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); + mIconsView = checkNotNull(preferenceView.findViewById(R.id.circles_container)); + mIconsView.setUiExecutor(MoreExecutors.directExecutor()); preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); @@ -142,9 +147,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); - assertThat(mPreference.getLoadedIcons().icons().stream() + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(2); + assertThat(mIconsView.getDisplayedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(2, 3).inOrder(); @@ -162,9 +167,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(4); - assertThat(mPreference.getLoadedIcons().icons().stream() + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(4); + assertThat(mIconsView.getDisplayedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(1, 2, 3, 4).inOrder(); @@ -182,8 +187,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); verify(mHelperBackend, never()).getContactPhoto(any()); } @@ -201,8 +206,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(3); verify(mConversationIconFactory, times(3)).getConversationDrawable((ShortcutInfo) any(), any(), anyInt(), anyBoolean()); } From 1afff118cda9e4b96a59db1633fe2d57da41f84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Fri, 16 Aug 2024 19:02:32 +0200 Subject: [PATCH 8/9] Replace the TYPE_UNKNOWN icon, drop the rabbit, keep the lotus flower Fixes: 360360424 Test: Manual Flag: android.app.modes_ui Change-Id: I9a48453aa27e7610e27cdb62e6dc1a212ade3a75 --- res/values/arrays.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/values/arrays.xml b/res/values/arrays.xml index f8337b3035c..8cbf5dc6ad3 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -1554,7 +1554,7 @@ @*android:drawable/ic_zen_mode_type_theater @*android:drawable/ic_zen_mode_icon_book - @*android:drawable/ic_zen_mode_type_unknown + @*android:drawable/ic_zen_mode_icon_lotus_flower @*android:drawable/ic_zen_mode_type_immersive @*android:drawable/ic_zen_mode_icon_headphones @*android:drawable/ic_zen_mode_icon_tv @@ -1565,10 +1565,10 @@ @*android:drawable/ic_zen_mode_icon_fork_and_knife @*android:drawable/ic_zen_mode_icon_shopping_cart @*android:drawable/ic_zen_mode_icon_child - @*android:drawable/ic_zen_mode_icon_rabbit @*android:drawable/ic_zen_mode_icon_animal_paw - @*android:drawable/ic_zen_mode_type_managed + @*android:drawable/ic_zen_mode_type_unknown + @*android:drawable/ic_zen_mode_type_managed @*android:drawable/ic_zen_mode_type_other @*android:drawable/ic_zen_mode_icon_heart @*android:drawable/ic_zen_mode_icon_house @@ -1616,10 +1616,10 @@ Fork and knife Shopping cart Child - Rabbit Animal paw - Supervisor + Star badge + Two people Star Heart House From 70a05ec421952c82869caf829e274a55bd3e7347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Fri, 16 Aug 2024 19:50:17 +0200 Subject: [PATCH 9/9] Use the new inspirational text in mode interstitials Also use the OTHER image for calendar/time, instead of UNKNOWN. Bug: 332730534 Test: manual Flag: android.app.modes_ui Change-Id: I85a36d2d409ad3e9f3d5f70f95d90512443971a7 --- res/values/strings.xml | 17 +++++++++ .../modes/SetupInterstitialActivity.java | 35 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 6c018c23ec9..20e4ce8ee01 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9523,6 +9523,23 @@ Minimize interruptions by only allowing important people and apps to reach you + + Set a mode that follows a regular schedule + + Keep your device in sync with your day’s events + + Wake up feeling like 100% + + Put safety first while on the road + + Gain focus to get in the zone + + For moments when courtesy counts + + Guided usage to help you stay in good hands + + Take control of your attention + Warning diff --git a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java index f26de76844b..984fa1ccfc6 100644 --- a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java +++ b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java @@ -21,9 +21,13 @@ import static android.app.AutomaticZenRule.TYPE_DRIVING; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; import static android.app.AutomaticZenRule.TYPE_MANAGED; import static android.app.AutomaticZenRule.TYPE_OTHER; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; +import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import android.annotation.SuppressLint; import android.app.ActionBar; import android.content.Context; import android.content.Intent; @@ -38,6 +42,7 @@ import android.widget.Toolbar; import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; @@ -123,6 +128,11 @@ public class SetupInterstitialActivity extends FragmentActivity { title.setText(mode.getName()); } + TextView subtitle = findViewById(R.id.mode_name_subtitle); + if (subtitle != null) { + subtitle.setText(getSubtitle(mode)); + } + ImageView img = findViewById(R.id.image); if (img != null) { setImage(img, mode); @@ -134,6 +144,27 @@ public class SetupInterstitialActivity extends FragmentActivity { } } + @StringRes + @SuppressLint("SwitchIntDef") + private static int getSubtitle(ZenMode mode) { + if (mode.isSystemOwned()) { + return switch (mode.getType()) { + case TYPE_SCHEDULE_TIME -> R.string.zen_mode_inspiration_schedule_time; + case TYPE_SCHEDULE_CALENDAR -> R.string.zen_mode_inspiration_schedule_calendar; + default -> R.string.zen_mode_inspiration_generic; // Custom Manual + }; + } else { + return switch (mode.getType()) { + case TYPE_BEDTIME -> R.string.zen_mode_inspiration_bedtime; + case TYPE_DRIVING -> R.string.zen_mode_inspiration_driving; + case TYPE_IMMERSIVE -> R.string.zen_mode_inspiration_immersive; + case TYPE_THEATER -> R.string.zen_mode_inspiration_theater; + case TYPE_MANAGED -> R.string.zen_mode_inspiration_managed; + default -> R.string.zen_mode_inspiration_generic; // Including OTHER, UNKNOWN. + }; + } + } + private void setImage(@NonNull ImageView img, @NonNull ZenMode mode) { int drawableRes = switch (mode.getType()) { case TYPE_BEDTIME -> R.drawable.modes_interstitial_bedtime; @@ -141,7 +172,9 @@ public class SetupInterstitialActivity extends FragmentActivity { case TYPE_IMMERSIVE -> R.drawable.modes_interstitial_immersive; case TYPE_THEATER -> R.drawable.modes_interstitial_theater; case TYPE_MANAGED -> R.drawable.modes_interstitial_managed; - case TYPE_OTHER -> R.drawable.modes_interstitial_other; + case TYPE_OTHER, TYPE_SCHEDULE_CALENDAR, TYPE_SCHEDULE_TIME -> + R.drawable.modes_interstitial_other; + case TYPE_UNKNOWN -> R.drawable.modes_interstitial_unknown; default -> R.drawable.modes_interstitial_unknown; };