Snap for 12241618 from 4cffde0188 to 24Q4-release

Change-Id: I0972b0440315b2aeba62b629ec3cae971112a3d3
This commit is contained in:
Android Build Coastguard Worker
2024-08-17 01:21:40 +00:00
82 changed files with 1927 additions and 1188 deletions

View File

@@ -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"
}

View File

@@ -58,8 +58,8 @@
android:lineBreakWordStyle="phrase"
android:maxLines="10"/>
<!-- Circular icons (32dp) will be ImageViews under this LinearLayout -->
<LinearLayout
<!-- Circular icons (32dp) will be ImageViews under this container -->
<com.android.settings.notification.modes.CircularIconsView
android:id="@+id/circles_container"
android:importantForAccessibility="noHideDescendants"
android:orientation="horizontal"

View File

@@ -1,24 +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.
-->
<Spinner
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:theme="@style/Widget.PopupWindow.Settings" />

View File

@@ -1554,7 +1554,7 @@
<item>@*android:drawable/ic_zen_mode_type_theater</item> <!-- Film reel -->
<item>@*android:drawable/ic_zen_mode_icon_book</item>
<!-- Wellbeing -->
<item>@*android:drawable/ic_zen_mode_type_unknown</item> <!-- Lotus flower -->
<item>@*android:drawable/ic_zen_mode_icon_lotus_flower</item>
<item>@*android:drawable/ic_zen_mode_type_immersive</item>
<item>@*android:drawable/ic_zen_mode_icon_headphones</item>
<item>@*android:drawable/ic_zen_mode_icon_tv</item>
@@ -1565,10 +1565,10 @@
<item>@*android:drawable/ic_zen_mode_icon_fork_and_knife</item>
<item>@*android:drawable/ic_zen_mode_icon_shopping_cart</item>
<item>@*android:drawable/ic_zen_mode_icon_child</item>
<item>@*android:drawable/ic_zen_mode_icon_rabbit</item>
<item>@*android:drawable/ic_zen_mode_icon_animal_paw</item>
<!-- Generic / abstract -->
<item>@*android:drawable/ic_zen_mode_type_managed</item> <!-- Account -->
<item>@*android:drawable/ic_zen_mode_type_unknown</item> <!-- Star badge -->
<item>@*android:drawable/ic_zen_mode_type_managed</item> <!-- Two people / Supervisor -->
<item>@*android:drawable/ic_zen_mode_type_other</item> <!-- Star -->
<item>@*android:drawable/ic_zen_mode_icon_heart</item>
<item>@*android:drawable/ic_zen_mode_icon_house</item>
@@ -1616,10 +1616,10 @@
<item>Fork and knife</item>
<item>Shopping cart</item>
<item>Child</item>
<item>Rabbit</item>
<item>Animal paw</item>
<!-- Generic / abstract -->
<item>Supervisor</item>
<item>Star badge</item>
<item>Two people</item>
<item>Star</item>
<item>Heart</item>
<item>House</item>

View File

@@ -9535,6 +9535,23 @@
<!-- Priority Modes: Blurb for modes that are not of a specific type (OTHER, UNKNOWN). [CHAR LIMIT=NONE] -->
<string name="zen_mode_blurb_generic">Minimize interruptions by only allowing important people and apps to reach you</string>
<!-- Priority Modes: Inspirational text for a mode that activates during a fixed time schedule (e.g. 9:00-17:00). [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_schedule_time">Set a mode that follows a regular schedule</string>
<!-- Priority Modes: Inspirational text for a mode that activates during calendar events. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_schedule_calendar">Keep your device in sync with your days events</string>
<!-- Priority Modes: Inspirational text for a mode of type BEDTIME. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_bedtime">Wake up feeling like 100%</string>
<!-- Priority Modes: Inspirational text for a mode of type DRIVING. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_driving">Put safety first while on the road</string>
<!-- Priority Modes: Inspirational text for a mode of type IMMERSIVE. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_immersive">Gain focus to get in the zone</string>
<!-- Priority Modes: Inspirational text for a mode of type THEATER. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_theater">For moments when courtesy counts</string>
<!-- Priority Modes: Inspirational text for a mode of type MANAGED. [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_managed">Guided usage to help you stay in good hands</string>
<!-- Priority Modes: Inspirational text for modes that are not of a specific type (OTHER, UNKNOWN). [CHAR LIMIT=NONE] -->
<string name="zen_mode_inspiration_generic">Take control of your attention</string>
<!-- Content description for help icon button [CHAR LIMIT=20] -->
<string name="warning_button_text">Warning</string>

View File

@@ -57,7 +57,7 @@
"com.android.settings.fuelgauge.batteryusage.BatteryUsageBreakdownController"
settings:isPreferenceVisible="false">
<com.android.settings.fuelgauge.batteryusage.SpinnerPreference
<com.android.settingslib.widget.SettingsSpinnerPreference
android:key="battery_usage_spinner"
settings:isPreferenceVisible="false" />

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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<String, Integer> 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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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<List<FingerprintData>?>
/** Indicates if a user can enroll another fingerprint */
val canEnrollUser: Flow<Boolean>
fun maxFingerprintsEnrollable(): Int
}
class FingerprintEnrollmentRepositoryImpl(
fingerprintManager: FingerprintManager,
userRepo: UserRepo,
private val settingsRepository: FingerprintSettingsRepository,
backgroundDispatcher: CoroutineDispatcher,
applicationScope: CoroutineScope,
) : FingerprintEnrollmentRepository {
private val enrollmentChangedFlow: Flow<Int?> =
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<List<FingerprintData>> =
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<Boolean> =
currentEnrollments.map {
it?.size?.let { it < settingsRepository.maxEnrollableFingerprints() } ?: false
}
override fun maxFingerprintsEnrollable(): Int {
return settingsRepository.maxEnrollableFingerprints()
}
}

View File

@@ -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<FingerprintSensor>
/** Indicates if this device supports the side fingerprint sensor */
val hasSideFps: Flow<Boolean>
}
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<FingerprintSensor> =
fingerprintPropsInternal.transform { emit(it.toFingerprintSensor()) }
override val hasSideFps: Flow<Boolean> =
fingerprintSensor.flatMapLatest { flow { emit(fingerprintManager.isPowerbuttonFps()) } }
companion object {
private val DEFAULT_PROPS =

View File

@@ -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
}

View File

@@ -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

View File

@@ -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<Int>
}
class UserRepoImpl(val currUser: Int): UserRepo {
override val currentUser: Flow<Int> = flowOf(currUser)
}

View File

@@ -97,6 +97,8 @@ class UdfpsEnrollDebugRepositoryImpl :
}
override val fingerprintSensor: Flow<FingerprintSensor> = flowOf(sensorProps)
override val hasSideFps: Flow<Boolean>
get() = flowOf(false)
private fun pointToLeftOfSensor(sensorLocation: Rect): MotionEvent =
MotionEvent.obtain(

View File

@@ -26,4 +26,4 @@ class DebugTouchEventInteractorImpl(
) : TouchEventInteractor {
override val touchEvent: Flow<MotionEvent> =
udfpsSimulatedTouchEventsRepository.touchExplorationDebug
}
}

View File

@@ -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<FingerprintAuthAttemptModel> ->
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"
}
}

View File

@@ -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<Boolean> = fingerprintEnrollmentRepository.canEnrollUser
/** Indicates the maximum fingerprints enrollable for a given user */
override fun maxFingerprintsEnrollable(): Int {
return fingerprintEnrollmentRepository.maxFingerprintsEnrollable()
}
}

View File

@@ -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<FingerEnrollState> = 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"
}
}

View File

@@ -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<List<FingerprintData>?> = flow {
emit(
fingerprintManager
.getEnrolledFingerprints(userId)
?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }
?.toList()
)
}
}

View File

@@ -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,

View File

@@ -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<Long, ByteArray> =
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<List<FingerprintData>?> = flow {
emit(
fingerprintManager?.getEnrolledFingerprints(applicationContext.userId)
?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }?.toList()
)
}
override val canEnrollFingerprints: Flow<Boolean> = 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<FingerEnrollState> =
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<FingerprintAuthAttemptModel> ->
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,
)
}
}

View File

@@ -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<FingerprintSensor>

View File

@@ -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<Long, ByteArray> =
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)
}
}

View File

@@ -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,
)
}
}

View File

@@ -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) }
}
}

View File

@@ -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<Boolean> = repo.hasSideFps
}

View File

@@ -24,4 +24,3 @@ interface TouchEventInteractor {
/** A flow simulating user touches. */
val touchEvent: Flow<MotionEvent>
}

View File

@@ -13,6 +13,6 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest
package="com.android.settings.biometrics.fingerprint2.lib">
</manifest>

View File

@@ -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
}

View File

@@ -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<Boolean>
/** Indicates the maximum fingerprints enrollable for a given user */
fun maxFingerprintsEnrollable(): Int
}

View File

@@ -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<FingerEnrollState>
}

View File

@@ -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<List<FingerprintData>?>
}

View File

@@ -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<List<FingerprintData>?>
/** Returns the max enrollable fingerprints, note during SUW this might be 1 */
val maxEnrollableFingerprints: Flow<Int>
/** Returns true if a user can enroll a fingerprint false otherwise. */
val canEnrollFingerprints: Flow<Boolean>
/** Retrieves the sensor properties of a device */
val sensorPropertiesInternal: Flow<FingerprintSensor?>
/** 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<Long, ByteArray>
/**
* 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<FingerEnrollState>
/**
* 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?
}

View File

@@ -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<Long, ByteArray>
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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<FingerprintSensor?>
/** Indicates if the device supports side fps */
val hasSideFps: Flow<Boolean>
}

View File

@@ -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

View File

@@ -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<FingerEnrollState?> =
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(),
)
}
}

View File

@@ -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<FingerEnrollState?> =
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<MotionEvent?> = MutableStateFlow(null)
val touchEvent =
_touchEvent.asStateFlow().filterNotNull()
val touchEvent = _touchEvent.asStateFlow().filterNotNull()
/** Determines the current [EnrollStageModel] enrollment is in */
private val enrollStage: Flow<EnrollStageModel> =
@@ -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(),
)
}
}

View File

@@ -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<Boolean> = fingerprintInteractor.canEnrollFingerprints
val isAddAnotherButtonVisible: Flow<Boolean> =
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(),
)
}
}

View File

@@ -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<FingerprintSensorType> =
fingerprintManagerInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType }
sensorInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType }
private val _isUdfps: Flow<Boolean> =
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(),
)
}
}

View File

@@ -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<FingerprintSensor?> = fingerprintManagerInteractor.sensorPropertiesInternal
val sensor: Flow<FingerprintSensor?> = sensorInteractor.sensorPropertiesInternal
/** Represents a stream of [FingerprintFlow] */
val fingerprintFlow: Flow<FingerprintFlow?> = fingerprintFlowViewModel.fingerprintFlow
@@ -67,7 +67,7 @@ class FingerprintEnrollIntroViewModel(
FingerprintEnrollIntroViewModel(
provider[FingerprintNavigationViewModel::class],
provider[FingerprintFlowViewModel::class],
biometricEnvironment!!.fingerprintManagerInteractor,
biometricEnvironment!!.createSensorPropertiesInteractor(),
)
}
}

View File

@@ -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<FingerprintSensorType?> =
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(),
)
}
}

View File

@@ -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<GatekeeperInfo?> = 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())
}
}
}

View File

@@ -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<FingerprintFlow?> = 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())
}
}
}

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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<List<FingerprintData>?> =
MutableStateFlow(null)
@@ -62,19 +72,18 @@ class FingerprintSettingsViewModel(
/** Represents the stream of the information of "Add Fingerprint" preference. */
val addFingerprintPrefInfo: Flow<Pair<Boolean, Int>> =
_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<Boolean> =
_enrolledFingerprints.filterOnlyWhenSettingsIsShown().transform {
emit(fingerprintManagerInteractor.hasSideFps() == true && !it.isNullOrEmpty())
_enrolledFingerprints.filterOnlyWhenSettingsIsShown().combine(sensorInteractor.hasSideFps) {
fingerprints,
hasSideFps ->
hasSideFps && !fingerprints.isNullOrEmpty()
}
private val _isShowingDialog: MutableStateFlow<PreferenceViewModel?> = MutableStateFlow(null)
@@ -90,10 +99,10 @@ class FingerprintSettingsViewModel(
private val _consumerShouldAuthenticate: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _fingerprintSensorType: Flow<FingerprintSensorType> =
fingerprintManagerInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType }
sensorInteractor.sensorPropertiesInternal.filterNotNull().map { it.sensorType }
private val _sensorNullOrEmpty: Flow<Boolean> =
fingerprintManagerInteractor.sensorPropertiesInternal.map { it == null }
sensorInteractor.sensorPropertiesInternal.map { it == null }
private val _isLockedOut: MutableStateFlow<FingerprintAuthAttemptModel.Error?> =
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
}

View File

@@ -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<BatteryDiffEntry> 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<String, Preference> mPreferenceCache = new ArrayMap<>();
private int mSpinnerPosition;
private String mSlotInformation;
private SettingsSpinnerPreference mSpinnerPreference;
private SettingsSpinnerAdapter<CharSequence> 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);
}
/**

View File

@@ -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<CharSequence> {
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];
}
}
}

View File

@@ -61,13 +61,6 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
LayoutPreference preference = checkNotNull(screen.findPreference(getPreferenceKey()));
preference.setSelectable(false);
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
}
ImageView iconView = checkNotNull(preference.findViewById(R.id.entity_header_icon));
ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
if (layoutParams.width != iconSizePx || layoutParams.height != iconSizePx) {
@@ -75,6 +68,14 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
layoutParams.height = iconSizePx;
iconView.setLayoutParams(layoutParams);
}
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
mHeaderController.done(false); // Make the space for the (unused) name go away.
}
}
protected void updateIcon(Preference preference, @NonNull ZenMode zenMode,

View File

@@ -16,236 +16,72 @@
package com.android.settings.notification.modes;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
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.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import com.google.common.base.Equivalence;
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 CircularIconsPreference extends RestrictedPreference {
private static final float DISABLED_ITEM_ALPHA = 0.3f;
record LoadedIcons(ImmutableList<Drawable> 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<List<Drawable>> 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);
}
<T> void displayIcons(CircularIconSet<T> iconSet) {
displayIcons(iconSet, null);
<T> void setIcons(CircularIconSet<T> iconSet) {
setIcons(iconSet, null);
}
<T> void displayIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
<T> void setIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> 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<ListenableFuture<Drawable>> 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 <numImages> 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<? extends View> 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);
}
}

View File

@@ -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<Drawable> icons, int extraItems) { }
private Executor mUiExecutor;
private int mNumberOfCirclesThatFit;
// Chronologically, fields will be set top-to-bottom.
@Nullable private CircularIconSet<?> mIconSet;
@Nullable private ListenableFuture<List<Drawable>> 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;
}
<T> void setIcons(CircularIconSet<T> 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<ListenableFuture<Drawable>> 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 <numImages> 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<? extends View> 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;
}
}

View File

@@ -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 });

View File

@@ -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.app.settings.SettingsEnums;
import android.content.Context;
@@ -39,6 +43,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;
@@ -124,6 +129,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);
@@ -135,6 +145,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;
@@ -142,7 +173,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;
};

View File

@@ -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<AppEntry> 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);
}

View File

@@ -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<Integer> getSoundIcons(ZenPolicy policy) {

View File

@@ -96,7 +96,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);
}

View File

@@ -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<WifiAvailableChannel> 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
+ '}';
}
}

View File

@@ -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));
}
}

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new);
CircularIconSet<Integer> 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<Integer> one = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Drawable> getIcons(ViewGroup container) {
private static List<Drawable> getDrawables(ViewGroup container) {
ArrayList<Drawable> drawables = new ArrayList<>();
for (int i = 0; i < container.getChildCount(); i++) {
if (container.getChildAt(i) instanceof ImageView imageView) {

View File

@@ -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

View File

@@ -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;
@@ -52,6 +53,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;
@@ -82,6 +84,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
private ZenModeAppsLinkPreferenceController mController;
private CircularIconsPreference mPreference;
private CircularIconsView mIconsView;
private Context mContext;
@Mock
@@ -114,6 +117,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);
@@ -273,7 +278,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
@@ -313,7 +318,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);
@@ -322,7 +327,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
@@ -343,11 +348,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);
@@ -356,7 +361,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

View File

@@ -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()));
}
}

View File

@@ -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());
}

View File

@@ -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
}

View File

@@ -111,9 +111,10 @@ class Injector(step: FingerprintNavigationStep.UiStep) {
var fingerprintEnrollViewModel =
FingerprintEnrollViewModel(
fingerprintManagerInteractor,
gatekeeperViewModel,
navigationViewModel,
fingerprintManagerInteractor,
fingerprintManagerInteractor,
)
var fingerprintEnrollEnrollingViewModel =

View File

@@ -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<FingerprintData> = mutableListOf()
@@ -67,19 +82,22 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
override val enrolledFingerprints: Flow<List<FingerprintData>> = flow {
emit(enrolledFingerprintsInternal)
}
override val canEnrollFingerprints: Flow<Boolean> = flow {
emit(enrolledFingerprintsInternal.size < enrollableFingerprints)
}
override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow { emit(sensorProp) }
override fun maxFingerprintsEnrollable(): Int {
return enrollableFingerprints
}
override val maxEnrollableFingerprints: Flow<Int> = flow { emit(enrollableFingerprints) }
override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow { emit(sensorProp) }
override val hasSideFps: Flow<Boolean> =
flowOf(sensorProp.sensorType == FingerprintSensorType.POWER_BUTTON)
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
fingerprintEnrollOptions: FingerprintEnrollOptions
fingerprintEnrollOptions: FingerprintEnrollOptions,
): Flow<FingerEnrollState> = 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
}
}

View File

@@ -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<Int> = currUser
}
@Before
fun setup() {
@@ -89,7 +117,7 @@ class FingerprintManagerInteractorTest {
FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
maxFingerprints,
listOf<ComponentInfoInternal>(),
FingerprintSensorProperties.TYPE_POWER_BUTTON,
false /* halControlsIllumination */,
@@ -97,20 +125,37 @@ class FingerprintManagerInteractorTest {
listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
)
.toFingerprintSensor()
val fingerprintSensorRepository =
object : FingerprintSensorRepository {
override val fingerprintSensor: Flow<FingerprintSensor> = flowOf(sensor)
override val hasSideFps: Flow<Boolean> = 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<Fingerprint> = emptyList()
assertThat(underTest.enrolledFingerprints.last()).isEqualTo(emptyFingerprintList)
assertThat(enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last())
.isEqualTo(emptyFingerprintList)
}
@Test
@@ -129,7 +175,7 @@ class FingerprintManagerInteractorTest {
val fingerprintList: List<Fingerprint> = 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<Fingerprint> =
val fingerprintList: List<Fingerprint> =
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<Fingerprint> =
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<Fingerprint> =
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<Long, ByteArray?>? = 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<FingerprintManager.AuthenticationCallback> = 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<FingerprintManager.AuthenticationCallback> = 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 }
}

View File

@@ -99,9 +99,10 @@ class FingerprintEnrollFindSensorViewModelV2Test {
backgroundViewModel.inForeground()
enrollViewModel =
FingerprintEnrollViewModel(
fakeFingerprintManagerInteractor,
gatekeeperViewModel,
navigationViewModel,
fakeFingerprintManagerInteractor,
fakeFingerprintManagerInteractor,
)
accessibilityInteractor =
object : AccessibilityInteractor {

View File

@@ -49,8 +49,7 @@ class RFPSIconTouchViewModelTest {
fun setup() {
Dispatchers.setMain(backgroundDispatcher)
testScope = TestScope(backgroundDispatcher)
rfpsIconTouchViewModel =
RFPSIconTouchViewModel()
rfpsIconTouchViewModel = RFPSIconTouchViewModel()
}
@After

View File

@@ -88,9 +88,10 @@ class FingerprintEnrollEnrollingViewModelTest {
backgroundViewModel.inForeground()
val fingerprintEnrollViewModel =
FingerprintEnrollViewModel(
fakeFingerprintManagerInteractor,
gateKeeperViewModel,
navigationViewModel,
fakeFingerprintManagerInteractor,
fakeFingerprintManagerInteractor,
)
enrollEnrollingViewModel =
FingerprintEnrollEnrollingViewModel(fingerprintEnrollViewModel, backgroundViewModel)

View File

@@ -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)

View File

@@ -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<FingerprintData> {
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
}

View File

@@ -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();