diff --git a/Android.bp b/Android.bp index 0a58ee8ea7c..28c3148cf28 100644 --- a/Android.bp +++ b/Android.bp @@ -130,6 +130,7 @@ android_library { "ims-common", ], flags_packages: [ + "aconfig_settings_flags", "android.app.flags-aconfig", ], } diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cc4d898403a..5072e677204 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -237,6 +237,7 @@ + diff --git a/res/layout/modes_edit_name.xml b/res/layout/modes_edit_name.xml index 0b086c746ed..7f1a1e606a3 100644 --- a/res/layout/modes_edit_name.xml +++ b/res/layout/modes_edit_name.xml @@ -20,7 +20,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingBottom="8dp"> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c9a67e462bb..5961b95a606 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -156,6 +156,7 @@ 2dp 12dp 12dp + -24dp 0dp 20dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 3d1c90c83c6..0392bfc52b3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9585,6 +9585,9 @@ Mode name + + Choose an icon + Calendar events diff --git a/res/xml/modes_edit_name_icon.xml b/res/xml/modes_edit_name_icon.xml index 2109c776ac8..4bcf67deccc 100644 --- a/res/xml/modes_edit_name_icon.xml +++ b/res/xml/modes_edit_name_icon.xml @@ -33,13 +33,15 @@ android:key="name" android:layout="@layout/modes_edit_name" /> - + - + + refreshExistingShortcuts(context)); enableTwoPaneDeepLinkActivityIfNecessary(pm, context); + storeSuwCompleteTimestamp(context, broadcast); } private void managedProfileSetup(Context context, final PackageManager pm, Intent broadcast, @@ -161,4 +163,10 @@ public class SettingsInitialize extends BroadcastReceiver { pm.setComponentEnabledSetting(searchStateReceiver, enableState, PackageManager.DONT_KILL_APP); } + + private void storeSuwCompleteTimestamp(Context context, Intent broadcast) { + if (SetupWizardUtils.ACTION_SETUP_WIZARD_FINISHED.equals(broadcast.getAction())) { + ElapsedTimeUtils.storeSuwFinishedTimestamp(context, System.currentTimeMillis()); + } + } } diff --git a/src/com/android/settings/SetupWizardUtils.java b/src/com/android/settings/SetupWizardUtils.java index 25e91598f3b..57adeee895d 100644 --- a/src/com/android/settings/SetupWizardUtils.java +++ b/src/com/android/settings/SetupWizardUtils.java @@ -32,6 +32,9 @@ import java.util.Arrays; public class SetupWizardUtils { + public static final String ACTION_SETUP_WIZARD_FINISHED = + "com.google.android.setupwizard.SETUP_WIZARD_FINISHED"; + public static String getThemeString(Intent intent) { String theme = intent.getStringExtra(WizardManagerHelper.EXTRA_THEME); if (theme == null) { diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index 6c16d94a51d..b837e1e9c5d 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -1034,6 +1034,9 @@ public class ManageApplications extends InstrumentedFragment } private void autoSetCollapsingToolbarLayoutScrolling() { + if (mAppBarLayout == null) { + return; + } final CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams(); final AppBarLayout.Behavior behavior = new AppBarLayout.Behavior(); diff --git a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt index e3233ed22b1..761a9c3a871 100644 --- a/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/BiometricsEnvironment.kt @@ -58,6 +58,7 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.SensorInte 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.UserInteractorImpl 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.AuthenitcateInteractor @@ -67,6 +68,7 @@ import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.Genera 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.domain.interactor.UserInteractor import com.android.settings.biometrics.fingerprint2.lib.model.Settings import java.util.concurrent.Executors import kotlinx.coroutines.MainScope @@ -97,11 +99,11 @@ class BiometricsEnvironment( 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 fingerprintEnrollmentRepository = + FingerprintEnrollmentRepositoryImpl(fingerprintManager, userRepo, fingerprintSettingsRepository, + backgroundDispatcher, applicationScope, fingerprintSensorRepository) private val debuggingRepository: DebuggingRepository = DebuggingRepositoryImpl() private val udfpsDebugRepo = UdfpsEnrollDebugRepositoryImpl() @@ -118,11 +120,13 @@ class BiometricsEnvironment( EnrollFingerprintInteractorImpl(context.userId, fingerprintManager, Settings) fun createFingerprintsEnrolledInteractor(): EnrolledFingerprintsInteractorImpl = - EnrolledFingerprintsInteractorImpl(fingerprintManager, context.userId) + EnrolledFingerprintsInteractorImpl(fingerprintEnrollmentRepository) fun createAuthenticateInteractor(): AuthenitcateInteractor = AuthenticateInteractorImpl(fingerprintManager, context.userId) + fun createUserInteractor(): UserInteractor = UserInteractorImpl(userRepo) + fun createRemoveFingerprintInteractor(): RemoveFingerprintInteractor = RemoveFingerprintsInteractorImpl(fingerprintManager, context.userId) diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt index 22904e9d2ac..0bb4eead62d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintEnrollmentRepo.kt @@ -23,14 +23,16 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine 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.flow.update import kotlinx.coroutines.withContext /** Repository that contains information about fingerprint enrollments. */ @@ -38,20 +40,31 @@ interface FingerprintEnrollmentRepository { /** The current enrollments of the user */ val currentEnrollments: Flow?> + /** Indicates the maximum fingerprints that are enrollable * */ + val maxFingerprintsEnrollable: Flow + /** Indicates if a user can enroll another fingerprint */ val canEnrollUser: Flow - fun maxFingerprintsEnrollable(): Int + /** + * Indicates if we should use the default settings for maximum enrollments or the sensor props + * from the fingerprint sensor + */ + fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) } class FingerprintEnrollmentRepositoryImpl( - fingerprintManager: FingerprintManager, + private val fingerprintManager: FingerprintManager, userRepo: UserRepo, - private val settingsRepository: FingerprintSettingsRepository, + settingsRepository: FingerprintSettingsRepository, backgroundDispatcher: CoroutineDispatcher, applicationScope: CoroutineScope, + sensorRepo: FingerprintSensorRepository, ) : FingerprintEnrollmentRepository { + private val _shouldUseSettingsMaxFingerprints = MutableStateFlow(false) + val shouldUseSettingsMaxFingerprints = _shouldUseSettingsMaxFingerprints.asStateFlow() + private val enrollmentChangedFlow: Flow = callbackFlow { val callback = @@ -72,27 +85,34 @@ class FingerprintEnrollmentRepositoryImpl( override val currentEnrollments: Flow> = userRepo.currentUser .distinctUntilChanged() - .flatMapLatest { currentUser -> - enrollmentChangedFlow.map { enrollmentChanged -> - if (enrollmentChanged == null || enrollmentChanged == currentUser) { - fingerprintManager - .getEnrolledFingerprints(currentUser) - ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } - ?.toList() - } else { - null - } - } - } + .combine(enrollmentChangedFlow) { currentUser, _ -> getFingerprintsForUser(currentUser) } .filterNotNull() .flowOn(backgroundDispatcher) - override val canEnrollUser: Flow = - currentEnrollments.map { - it?.size?.let { it < settingsRepository.maxEnrollableFingerprints() } ?: false + override val maxFingerprintsEnrollable: Flow = + shouldUseSettingsMaxFingerprints.combine(sensorRepo.fingerprintSensor) { + shouldUseSettings, + sensor -> + if (shouldUseSettings) { + settingsRepository.maxEnrollableFingerprints() + } else { + sensor.maxEnrollmentsPerUser + } } - override fun maxFingerprintsEnrollable(): Int { - return settingsRepository.maxEnrollableFingerprints() + override val canEnrollUser: Flow = + currentEnrollments.combine(maxFingerprintsEnrollable) { enrollments, maxFingerprints -> + enrollments.size < maxFingerprints + } + + override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) { + _shouldUseSettingsMaxFingerprints.update { useSettings } + } + + private fun getFingerprintsForUser(userId: Int): List? { + return fingerprintManager + .getEnrolledFingerprints(userId) + ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } + ?.toList() } } diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt index 720e7787d12..91260431bc5 100644 --- a/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/UserRepo.kt @@ -17,7 +17,10 @@ package com.android.settings.biometrics.fingerprint2.data.repository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.update /** * A repository responsible for indicating the current user. @@ -27,8 +30,18 @@ interface UserRepo { * This flow indicates the current user. */ val currentUser: Flow + + /** + * Updates the current user. + */ + fun updateUser(user: Int) } -class UserRepoImpl(val currUser: Int): UserRepo { - override val currentUser: Flow = flowOf(currUser) +class UserRepoImpl(currUser: Int): UserRepo { + private val _currentUser = MutableStateFlow(currUser) + override val currentUser = _currentUser.asStateFlow() + + override fun updateUser(user: Int) { + _currentUser.update { user } + } } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt index caeea4e4586..cfdfbe23081 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/CanEnrollFingerprintsInteractorImpl.kt @@ -21,11 +21,14 @@ import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.CanEnr import kotlinx.coroutines.flow.Flow class CanEnrollFingerprintsInteractorImpl( - val fingerprintEnrollmentRepository: FingerprintEnrollmentRepository + private val fingerprintEnrollmentRepository: FingerprintEnrollmentRepository ) : CanEnrollFingerprintsInteractor { override val canEnrollFingerprints: Flow = fingerprintEnrollmentRepository.canEnrollUser /** Indicates the maximum fingerprints enrollable for a given user */ - override fun maxFingerprintsEnrollable(): Int { - return fingerprintEnrollmentRepository.maxFingerprintsEnrollable() + override val maxFingerprintsEnrollable: Flow = + fingerprintEnrollmentRepository.maxFingerprintsEnrollable + + override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) { + fingerprintEnrollmentRepository.setShouldUseSettingsMaxFingerprints(useSettings) } } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt index 83b532ecd98..f8bcaf7d634 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrolledFingerprintsInteractorImpl.kt @@ -16,22 +16,14 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor -import android.hardware.fingerprint.FingerprintManager +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepository 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, + private val fingerprintEnrollmentRepository: FingerprintEnrollmentRepository ) : EnrolledFingerprintsInteractor { - override val enrolledFingerprints: Flow?> = flow { - emit( - fingerprintManager - .getEnrolledFingerprints(userId) - ?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) } - ?.toList() - ) - } + override val enrolledFingerprints: Flow?> = + fingerprintEnrollmentRepository.currentEnrollments } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UserInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UserInteractorImpl.kt new file mode 100644 index 00000000000..506006e8903 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UserInteractorImpl.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint2.domain.interactor + +import com.android.settings.biometrics.fingerprint2.data.repository.UserRepo +import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.UserInteractor +import kotlinx.coroutines.flow.Flow + +class UserInteractorImpl(private val userRepo: UserRepo) : UserInteractor { + override val currentUser: Flow = userRepo.currentUser + + override fun updateUser(user: Int) = userRepo.updateUser(user) +} diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt index 11a9258ed88..a5277a5e8a5 100644 --- a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/CanEnrollFingerprintsInteractor.kt @@ -23,5 +23,17 @@ interface CanEnrollFingerprintsInteractor { /** Returns true if a user can enroll a fingerprint false otherwise. */ val canEnrollFingerprints: Flow /** Indicates the maximum fingerprints enrollable for a given user */ - fun maxFingerprintsEnrollable(): Int + val maxFingerprintsEnrollable: Flow + + /** + * Indicates if we should use the default settings for maximum enrollments or the sensor props + * from the fingerprint sensor. This can be useful if you are supporting HIDL & AIDL enrollment + * types from one code base. Prior to AIDL there was no way to determine how many + * fingerprints were enrollable, Settings relied on + * com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser. + * + * Typically Fingerprints with AIDL HAL's should not use this + * (setShouldUseSettingsMaxFingerprints(false)) + */ + fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) } diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/UserInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/UserInteractor.kt new file mode 100644 index 00000000000..17b147a2f31 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/UserInteractor.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint2.lib.domain.interactor + +import kotlinx.coroutines.flow.Flow + +interface UserInteractor { + /** + * This flow indicates the current user. + */ + val currentUser: Flow + + /** + * Updates the current user. + */ + fun updateUser(user: Int) +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt index c306c7870b9..7aad16dce75 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/viewmodel/FingerprintSettingsViewModel.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -72,10 +71,12 @@ class FingerprintSettingsViewModel( /** Represents the stream of the information of "Add Fingerprint" preference. */ val addFingerprintPrefInfo: Flow> = - _enrolledFingerprints.filterOnlyWhenSettingsIsShown().combine( - canEnrollFingerprintsInteractor.canEnrollFingerprints - ) { _, canEnrollFingerprints -> - Pair(canEnrollFingerprints, canEnrollFingerprintsInteractor.maxFingerprintsEnrollable()) + combine( + _enrolledFingerprints.filterOnlyWhenSettingsIsShown(), + canEnrollFingerprintsInteractor.canEnrollFingerprints, + canEnrollFingerprintsInteractor.maxFingerprintsEnrollable, + ) { _, canEnrollFingerprints, maxFingerprints -> + Pair(canEnrollFingerprints, maxFingerprints) } /** Represents the stream of visibility of sfps preference. */ diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index 2c65934dd72..387bf837ce4 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -31,15 +31,12 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; import com.android.settings.R; import com.android.settings.SettingsActivity; @@ -71,8 +68,9 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere private volatile BluetoothDevice mJustBonded = null; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @VisibleForTesting @Nullable - private AlertDialog mProgressDialog = null; + ProgressDialogFragment mProgressDialog = null; @VisibleForTesting boolean mShouldTriggerAudioSharingShareThenPairFlow = false; private CopyOnWriteArrayList mDevicesWithMetadataChangedListener = @@ -384,41 +382,24 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere finish(); } - // TODO: use DialogFragment private void showConnectingDialog(@NonNull String deviceName) { postOnMainThread(() -> { String message = getContext().getString(R.string.progress_dialog_connect_device_content, deviceName); + if (mProgressDialog == null) { + mProgressDialog = ProgressDialogFragment.newInstance(this); + } if (mProgressDialog != null) { - Log.d(getLogTag(), "showConnectingDialog, is already showing"); - TextView textView = mProgressDialog.findViewById(R.id.message); - if (textView != null && !message.equals(textView.getText().toString())) { - Log.d(getLogTag(), "showConnectingDialog, update message"); - textView.setText(message); - } - return; + mProgressDialog.show(message); } - Log.d(getLogTag(), "showConnectingDialog, show dialog"); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = LayoutInflater.from(builder.getContext()); - View customView = inflater.inflate( - R.layout.dialog_audio_sharing_progress, /* root= */ - null); - TextView textView = customView.findViewById(R.id.message); - if (textView != null) { - textView.setText(message); - } - AlertDialog dialog = builder.setView(customView).setCancelable(false).create(); - dialog.setCanceledOnTouchOutside(false); - mProgressDialog = dialog; - dialog.show(); }); } private void dismissConnectingDialog() { postOnMainThread(() -> { if (mProgressDialog != null) { - mProgressDialog.dismiss(); + Log.d(getLogTag(), "Dismiss connecting dialog."); + mProgressDialog.dismissAllowingStateLoss(); } }); } diff --git a/src/com/android/settings/bluetooth/ProgressDialogFragment.java b/src/com/android/settings/bluetooth/ProgressDialogFragment.java new file mode 100644 index 00000000000..15d53299e42 --- /dev/null +++ b/src/com/android/settings/bluetooth/ProgressDialogFragment.java @@ -0,0 +1,133 @@ +/* + * 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.bluetooth; + +import android.app.Dialog; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +import com.google.common.base.Strings; + +public class ProgressDialogFragment extends InstrumentedDialogFragment { + private static final String TAG = "BTProgressDialog"; + + private static final String BUNDLE_KEY_MESSAGE = "bundle_key_message"; + + @Nullable private static FragmentManager sManager; + @Nullable private static Lifecycle sLifecycle; + private String mMessage = ""; + @Nullable private AlertDialog mAlertDialog; + + @Override + public int getMetricsCategory() { + // TODO: add metrics + return 0; + } + + /** + * Returns a new instance of {@link ProgressDialogFragment} dialog. + * + * @param host The Fragment this dialog will be hosted. + */ + @Nullable + public static ProgressDialogFragment newInstance(@Nullable Fragment host) { + if (host == null) return null; + try { + sManager = host.getChildFragmentManager(); + sLifecycle = host.getLifecycle(); + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to create new instance: " + e.getMessage()); + return null; + } + return new ProgressDialogFragment(); + } + + /** + * Display {@link ProgressDialogFragment} dialog. + * + * @param message The message to be shown on the dialog + */ + public void show(@NonNull String message) { + if (sManager == null) return; + Lifecycle.State currentState = sLifecycle == null ? null : sLifecycle.getCurrentState(); + if (currentState == null || !currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } + if (mAlertDialog != null && mAlertDialog.isShowing()) { + if (!mMessage.equals(message)) { + Log.d(TAG, "Update dialog message."); + TextView messageView = mAlertDialog.findViewById(R.id.message); + if (messageView != null) { + messageView.setText(message); + } + mMessage = message; + } + Log.d(TAG, "Dialog is showing, return."); + return; + } + mMessage = message; + Log.d(TAG, "Show up the progress dialog."); + Bundle args = new Bundle(); + args.putString(BUNDLE_KEY_MESSAGE, message); + setArguments(args); + show(sManager, TAG); + } + + /** Returns the current message on the dialog. */ + @VisibleForTesting + @NonNull + public String getMessage() { + return mMessage; + } + + private ProgressDialogFragment() { + } + + @Override + @NonNull + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Bundle args = requireArguments(); + String message = args.getString(BUNDLE_KEY_MESSAGE, ""); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + View customView = inflater.inflate( + R.layout.dialog_audio_sharing_progress, /* root= */ null); + TextView textView = customView.findViewById(R.id.message); + if (textView != null && !Strings.isNullOrEmpty(message)) { + textView.setText(message); + } + AlertDialog dialog = builder.setView(customView).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + mAlertDialog = dialog; + return dialog; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 1b68eaccbfe..54a758c82d3 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.bluetooth.BluetoothPairingDetail; @@ -95,6 +96,11 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } sHost = host; sListener = listener; sEventData = eventData; diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java index 7d9164449aa..fbd2e635f82 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -92,6 +93,11 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); if (dialog != null) { int newGroupId = BluetoothUtils.getGroupId(newDevice); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingErrorDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingErrorDialogFragment.java index e842b37eef7..94d4a698623 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingErrorDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingErrorDialogFragment.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -53,6 +54,11 @@ public class AudioSharingErrorDialogFragment extends InstrumentedDialogFragment Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); if (dialog != null) { Log.d(TAG, "Dialog is showing, return."); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java index a8ad70b3695..e8ab716fe5f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java @@ -26,6 +26,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -69,6 +70,11 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } sListener = listener; AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); if (dialog != null) { diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java index ef461ebf1a0..a952c488156 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.bluetooth.Utils; @@ -89,6 +90,11 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } sListener = listener; sNewDevice = newDevice; sEventData = eventData; diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingProgressDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingProgressDialogFragment.java index 53bfcf8f17c..840c7bbeb45 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingProgressDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingProgressDialogFragment.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -72,6 +73,11 @@ public class AudioSharingProgressDialogFragment extends InstrumentedDialogFragme Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); if (dialog != null) { if (!sMessage.equals(message)) { @@ -80,6 +86,7 @@ public class AudioSharingProgressDialogFragment extends InstrumentedDialogFragme if (messageView != null) { messageView.setText(message); } + sMessage = message; } Log.d(TAG, "Dialog is showing, return."); return; diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java index 5b71f5163e9..2bd79c942bb 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -89,6 +90,11 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + Lifecycle.State currentState = host.getLifecycle().getCurrentState(); + if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + Log.d(TAG, "Fail to show dialog with state: " + currentState); + return; + } AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); if (dialog != null) { int newGroupId = BluetoothUtils.getGroupId(newDevice); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index b91a1f1df04..14da750d736 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -767,7 +767,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController && !(fragment instanceof AudioSharingErrorDialogFragment) && ((DialogFragment) fragment).getDialog() != null) { Log.d(TAG, "Remove stale dialog = " + fragment.getTag()); - ((DialogFragment) fragment).dismiss(); + ((DialogFragment) fragment).dismissAllowingStateLoss(); } } } diff --git a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java index 3cb30251c51..c93b450ba01 100644 --- a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java @@ -15,13 +15,16 @@ package com.android.settings.location; import static android.Manifest.permission_group.LOCATION; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.icu.text.RelativeDateTimeFormatter; +import android.location.LocationManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; import android.provider.Settings; +import android.util.Log; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -43,6 +46,8 @@ import java.util.List; * Preference controller that handles the display of apps that access locations. */ public class RecentLocationAccessPreferenceController extends LocationBasePreferenceController { + private static final String TAG = RecentLocationAccessPreferenceController.class + .getSimpleName(); public static final int MAX_APPS = 3; @VisibleForTesting RecentAppOpsAccess mRecentLocationApps; @@ -51,7 +56,8 @@ public class RecentLocationAccessPreferenceController extends LocationBasePrefer private boolean mShowSystem = false; private boolean mSystemSettingChanged = false; - private static class PackageEntryClickedListener implements + @VisibleForTesting + static class PackageEntryClickedListener implements Preference.OnPreferenceClickListener { private final Context mContext; private final String mPackage; @@ -66,12 +72,28 @@ public class RecentLocationAccessPreferenceController extends LocationBasePrefer @Override public boolean onPreferenceClick(Preference preference) { - final Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); - intent.setPackage(mContext.getPackageManager().getPermissionControllerPackageName()); - intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, LOCATION); - intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackage); - intent.putExtra(Intent.EXTRA_USER, mUserHandle); - mContext.startActivity(intent); + if (mPackage.equals(mContext.getSystemService(LocationManager.class) + .getExtraLocationControllerPackage())) { + try { + mContext.startActivityAsUser( + new Intent(Settings.ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS), + mUserHandle); + } catch (ActivityNotFoundException e) { + // In rare cases where location controller extra package is set, but + // no activity exists to handle the location controller extra package settings + // intent, log an error instead of crashing. + Log.e(TAG, "No activity to handle " + + "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS"); + } + } else { + final Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); + intent.setPackage(mContext.getPackageManager() + .getPermissionControllerPackageName()); + intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, LOCATION); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackage); + intent.putExtra(Intent.EXTRA_USER, mUserHandle); + mContext.startActivity(intent); + } return true; } } diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 6b5b4cb4d43..43bba0759a2 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -23,11 +23,13 @@ import android.util.Log import androidx.lifecycle.LifecycleOwner import com.android.settings.network.SubscriptionUtil import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged @@ -36,6 +38,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn private const val TAG = "SubscriptionRepository" @@ -132,20 +136,7 @@ class SubscriptionRepository(private val context: Context) { fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription() /** Flow for subscriptions changes. */ - fun subscriptionsChangedFlow() = callbackFlow { - val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - Dispatchers.Default.asExecutor(), - listener, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) } - }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) + fun subscriptionsChangedFlow() = getSharedSubscriptionsChangedFlow(context) /** Flow of active subscription ids. */ fun activeSubscriptionIdListFlow(): Flow> = @@ -172,6 +163,57 @@ class SubscriptionRepository(private val context: Context) { flowOf(null) } } + + companion object { + private lateinit var SharedSubscriptionsChangedFlow: Flow + + private fun getSharedSubscriptionsChangedFlow(context: Context): Flow { + if (!this::SharedSubscriptionsChangedFlow.isInitialized) { + SharedSubscriptionsChangedFlow = + context.applicationContext + .requireSubscriptionManager() + .subscriptionsChangedFlow() + .shareIn( + scope = CoroutineScope(Dispatchers.Default), + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + } + return SharedSubscriptionsChangedFlow + } + + /** + * Flow for subscriptions changes. + * + * Note: Even the SubscriptionManager.addOnSubscriptionsChangedListener's doc says the + * SubscriptionManager.OnSubscriptionsChangedListener.onSubscriptionsChanged() method will + * also be invoked once initially when calling it, there still case that the + * onSubscriptionsChanged() method is not invoked initially. For example, when the + * onSubscriptionsChanged event never happens before, on a device never ever has any + * subscriptions. + */ + private fun SubscriptionManager.subscriptionsChangedFlow() = + callbackFlow { + val listener = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + + override fun onAddListenerFailed() { + close() + } + } + + addOnSubscriptionsChangedListener(Dispatchers.Default.asExecutor(), listener) + + awaitClose { removeOnSubscriptionsChangedListener(listener) } + } + .onStart { emit(Unit) } // Ensure this flow is never empty + .conflate() + .onEach { Log.d(TAG, "subscriptions changed") } + .flowOn(Dispatchers.Default) + } } val Context.subscriptionManager: SubscriptionManager? diff --git a/tests/robotests/src/com/android/settings/SettingsInitializeTest.java b/tests/robotests/src/com/android/settings/SettingsInitializeTest.java index a8f42c2b7c7..467436b2566 100644 --- a/tests/robotests/src/com/android/settings/SettingsInitializeTest.java +++ b/tests/robotests/src/com/android/settings/SettingsInitializeTest.java @@ -24,6 +24,7 @@ import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import com.android.settings.core.instrumentation.ElapsedTimeUtils; import java.util.Collections; import org.junit.Before; import org.junit.Test; @@ -96,4 +97,12 @@ public class SettingsInitializeTest { assertThat(updatedShortcuts).hasSize(1); assertThat(updatedShortcuts.get(0)).isSameInstanceAs(info); } + + @Test + public void onReceive_suwFinished_shouldHaveElapsedTime() { + mSettingsInitialize.onReceive(mContext, new Intent(SetupWizardUtils.ACTION_SETUP_WIZARD_FINISHED)); + + final long elapsedTime = ElapsedTimeUtils.getElapsedTime(System.currentTimeMillis()); + assertThat(elapsedTime).isNotEqualTo(-1L); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 9f0cb6e35d9..949b3d83809 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -46,17 +46,21 @@ import android.os.Bundle; import android.os.Looper; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; -import android.widget.TextView; -import androidx.appcompat.app.AlertDialog; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.flags.Flags; @@ -73,8 +77,14 @@ import org.mockito.junit.MockitoRule; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Executor; /** Tests for {@link BluetoothDevicePairingDetailBase}. */ @@ -82,7 +92,7 @@ import java.util.concurrent.Executor; @Config(shadows = { ShadowBluetoothAdapter.class, ShadowAlertDialogCompat.class, - com.android.settings.testutils.shadow.ShadowFragment.class, + ShadowFragment.class, }) public class BluetoothDevicePairingDetailBaseTest { @@ -133,7 +143,6 @@ public class BluetoothDevicePairingDetailBaseTest { mFragment.mLocalManager = mLocalManager; mFragment.mBluetoothAdapter = mBluetoothAdapter; mFragment.initPreferencesFromPreferenceScreen(); - } @Test @@ -199,22 +208,26 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @Config(shadows = ShadowDialogFragment.class) public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); setUpFragmentWithPairAndJoinSharingIntent(true); mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); shadowOf(Looper.getMainLooper()).idle(); - AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - assertThat(dialog).isNotNull(); - TextView message = dialog.findViewById(R.id.message); - assertThat(message).isNotNull(); - assertThat(message.getText().toString()).isEqualTo( + ProgressDialogFragment progressDialog = mFragment.mProgressDialog; + assertThat(progressDialog).isNotNull(); + assertThat(progressDialog.getMessage()).isEqualTo( mContext.getString(R.string.progress_dialog_connect_device_content, TEST_DEVICE_ADDRESS)); + assertThat( + ShadowDialogFragment.isIsShowing(ProgressDialogFragment.class.getName())).isTrue(); verify(mFragment, never()).finish(); + + ShadowDialogFragment.reset(); } @Test @@ -283,9 +296,11 @@ public class BluetoothDevicePairingDetailBaseTest { } @Test + @Config(shadows = ShadowDialogFragment.class) public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + ShadowDialogFragment.reset(); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.mSelectedList.add(mBluetoothDevice); setUpFragmentWithPairAndJoinSharingIntent(true); @@ -309,6 +324,8 @@ public class BluetoothDevicePairingDetailBaseTest { assertThat(btDevice).isNotNull(); assertThat(btDevice).isEqualTo(mBluetoothDevice); verify(mFragment).finish(); + + ShadowDialogFragment.reset(); } @Test @@ -393,7 +410,13 @@ public class BluetoothDevicePairingDetailBaseTest { doReturn(intent).when(activity).getIntent(); doReturn(activity).when(mFragment).getActivity(); FragmentManager fragmentManager = mock(FragmentManager.class); + FragmentTransaction fragmentTransaction = mock(FragmentTransaction.class); + doReturn(fragmentTransaction).when(fragmentManager).beginTransaction(); doReturn(fragmentManager).when(mFragment).getFragmentManager(); + doReturn(fragmentManager).when(mFragment).getChildFragmentManager(); + Lifecycle lifecycle = mock(Lifecycle.class); + when(lifecycle.getCurrentState()).thenReturn(Lifecycle.State.RESUMED); + doReturn(lifecycle).when(mFragment).getLifecycle(); mFragment.mShouldTriggerAudioSharingShareThenPairFlow = mFragment.shouldTriggerAudioSharingShareThenPairFlow(); } @@ -425,4 +448,41 @@ public class BluetoothDevicePairingDetailBaseTest { return "test_tag"; } } + + /** Shadow of DialogFragment. */ + @Implements(value = DialogFragment.class) + public static class ShadowDialogFragment { + @RealObject + private DialogFragment mDialogFragment; + private static Map sDialogStatus = new HashMap<>(); + + /** Resetter of the shadow. */ + @Resetter + public static void reset() { + sDialogStatus.clear(); + } + + /** Implementation for DialogFragment#show. */ + @Implementation + public void show(@NonNull FragmentManager manager, @Nullable String tag) { + sDialogStatus.put(mDialogFragment.getClass().getName(), true); + } + + /** Implementation for DialogFragment#dismissAllowingStateLoss. */ + @Implementation + public void dismissAllowingStateLoss() { + sDialogStatus.put(mDialogFragment.getClass().getName(), false); + } + + /** Implementation for DialogFragment#dismiss. */ + @Implementation + public void dismiss() { + sDialogStatus.put(mDialogFragment.getClass().getName(), false); + } + + /** Check if DialogFragment is showing. */ + public static boolean isIsShowing(String clazzName) { + return sDialogStatus.getOrDefault(clazzName, false); + } + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ProgressDialogFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/ProgressDialogFragmentTest.java new file mode 100644 index 00000000000..74687767b47 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/ProgressDialogFragmentTest.java @@ -0,0 +1,140 @@ +/* + * 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; + +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowAlertDialogCompat.class}) +public class ProgressDialogFragmentTest { + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private static final String TEST_MESSAGE1 = "message1"; + private static final String TEST_MESSAGE2 = "message2"; + + private Fragment mParent; + + @Before + public void setUp() { + ShadowAlertDialogCompat.reset(); + mParent = new Fragment(); + FragmentController.setupFragment( + mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + } + + @After + public void tearDown() { + ShadowAlertDialogCompat.reset(); + } + + @Test + public void getMetricsCategory_correctValue() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(mParent); + // TODO: update real metric + assertThat(fragment.getMetricsCategory()).isEqualTo(0); + } + + @Test + public void onCreateDialog_unattachedFragment_nullDialogFragment() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(new Fragment()); + assertThat(fragment).isNull(); + } + + @Test + public void onCreateDialog_showDialog() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(mParent); + fragment.show(TEST_MESSAGE1); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + TextView view = dialog.findViewById(R.id.message); + assertThat(view).isNotNull(); + assertThat(view.getText().toString()).isEqualTo(TEST_MESSAGE1); + } + + @Test + public void dismissDialog_succeed() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(mParent); + fragment.show(TEST_MESSAGE1); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + + fragment.dismissAllowingStateLoss(); + shadowMainLooper().idle(); + assertThat(dialog.isShowing()).isFalse(); + } + + @Test + public void showDialog_sameMessage_keepExistingDialog() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(mParent); + fragment.show(TEST_MESSAGE1); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + + fragment.show(TEST_MESSAGE1); + shadowMainLooper().idle(); + assertThat(dialog.isShowing()).isTrue(); + TextView view = dialog.findViewById(R.id.message); + assertThat(view).isNotNull(); + assertThat(view.getText().toString()).isEqualTo(TEST_MESSAGE1); + } + + @Test + public void showDialog_newMessage_keepAndUpdateDialog() { + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(mParent); + fragment.show(TEST_MESSAGE1); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + assertThat(dialog.isShowing()).isTrue(); + TextView view = dialog.findViewById(R.id.message); + assertThat(view).isNotNull(); + assertThat(view.getText().toString()).isEqualTo(TEST_MESSAGE1); + + fragment.show(TEST_MESSAGE2); + shadowMainLooper().idle(); + assertThat(dialog.isShowing()).isTrue(); + assertThat(view.getText().toString()).isEqualTo(TEST_MESSAGE2); + } +} diff --git a/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java index e9284ee5b57..7673f38bccf 100644 --- a/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/location/RecentLocationAccessPreferenceControllerTest.java @@ -17,12 +17,15 @@ package com.android.settings.location; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; import android.os.UserHandle; import android.provider.Settings; import android.view.LayoutInflater; @@ -65,7 +68,8 @@ public class RecentLocationAccessPreferenceControllerTest { private DashboardFragment mDashboardFragment; @Mock private RecentAppOpsAccess mRecentLocationApps; - + @Mock + private LocationManager mLocationManager; private Context mContext; private RecentLocationAccessPreferenceController mController; private View mAppEntitiesHeaderView; @@ -130,4 +134,23 @@ public class RecentLocationAccessPreferenceControllerTest { mContext.getContentResolver(), Settings.Secure.LOCATION_SHOW_SYSTEM_OPS, 1); verify(mLayoutPreference, Mockito.times(1)).addPreference(Mockito.any()); } + + @Test + public void testPreferenceClick_onExtraLocationPackage_startsExtraLocationActivity() { + String extraLocationPkgName = "extraLocationPkgName"; + when(mContext.getSystemService(LocationManager.class)).thenReturn(mLocationManager); + when(mLocationManager.getExtraLocationControllerPackage()).thenReturn(extraLocationPkgName); + RecentLocationAccessPreferenceController.PackageEntryClickedListener listener = + new RecentLocationAccessPreferenceController.PackageEntryClickedListener( + mContext, extraLocationPkgName, UserHandle.CURRENT); + doNothing().when(mContext).startActivityAsUser(Mockito.refEq(new Intent( + Settings.ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS)), + Mockito.eq(UserHandle.CURRENT)); + + listener.onPreferenceClick(mLayoutPreference); + + verify(mContext).startActivityAsUser(Mockito.refEq(new Intent( + Settings.ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS)), + Mockito.eq(UserHandle.CURRENT)); + } } diff --git a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt index f61a3d3a02e..32ca2cdb59c 100644 --- a/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt +++ b/tests/shared/src/com/android/settings/testutils2/FakeFingerprintManagerInteractor.kt @@ -38,8 +38,12 @@ import com.android.systemui.biometrics.shared.model.FingerprintSensor import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.toFingerprintSensor import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.update /** Fake to be used by other classes to easily fake the FingerprintManager implementation. */ class FakeFingerprintManagerInteractor : @@ -52,7 +56,7 @@ class FakeFingerprintManagerInteractor : RenameFingerprintInteractor, SensorInteractor { - var enrollableFingerprints: Int = 5 + private val enrollableFingerprints = MutableStateFlow(5) var enrolledFingerprintsInternal: MutableList = mutableListOf() var challengeToGenerate: Pair = Pair(-1L, byteArrayOf()) var authenticateAttempt = FingerprintAuthAttemptModel.Success(1) @@ -82,13 +86,13 @@ class FakeFingerprintManagerInteractor : override val enrolledFingerprints: Flow> = flow { emit(enrolledFingerprintsInternal) } - override val canEnrollFingerprints: Flow = flow { - emit(enrolledFingerprintsInternal.size < enrollableFingerprints) + override val canEnrollFingerprints: Flow = enrollableFingerprints.transform { + emit(enrolledFingerprintsInternal.size < it) } - override fun maxFingerprintsEnrollable(): Int { - return enrollableFingerprints - } + override val maxFingerprintsEnrollable: Flow = enrollableFingerprints.asStateFlow() + + override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) {} override val sensorPropertiesInternal: Flow = flow { emit(sensorProp) } override val hasSideFps: Flow = @@ -110,4 +114,7 @@ class FakeFingerprintManagerInteractor : } } + fun setMaxEnrollableFingerprints(fingerprints: Int) { + enrollableFingerprints.update { fingerprints } + } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt index ca370829b6f..f47c6354498 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkSwitchControllerTest.kt @@ -62,10 +62,15 @@ class MobileNetworkSwitchControllerTest { on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false) } + private val mockSubscriptionActivationRepository = mock { + on { isActivationChangeableFlow() } doReturn flowOf(true) + } + private val controller = MobileNetworkSwitchController( context = context, preferenceKey = TEST_KEY, subscriptionRepository = mockSubscriptionRepository, + subscriptionActivationRepository = mockSubscriptionActivationRepository, ).apply { init(SUB_ID) } @Test diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt index 5052f57c588..ba5142e0eb0 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt @@ -91,7 +91,23 @@ class SubscriptionRepositoryTest { subInfoListener?.onSubscriptionsChanged() - assertThat(listDeferred.await()).hasSize(2) + assertThat(listDeferred.await().size).isAtLeast(2) + } + + @Test + fun subscriptionsChangedFlow_managerNotCallOnSubscriptionsChangedInitially() = runBlocking { + mockSubscriptionManager.stub { + on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer + { + subInfoListener = + it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener + // not call onSubscriptionsChanged here + } + } + + val initialValue = repository.subscriptionsChangedFlow().firstWithTimeoutOrNull() + + assertThat(initialValue).isSameInstanceAs(Unit) } @Test diff --git a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt index 691b6112bf7..2623206cddd 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/domain/interactor/FingerprintManagerInteractorTest.kt @@ -30,6 +30,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.CancellationSignal import android.os.Handler import com.android.settings.biometrics.GatekeeperPasswordProvider +import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintEnrollmentRepository 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.FingerprintSettingsRepositoryImpl @@ -61,7 +62,7 @@ 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.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -106,9 +107,14 @@ class FingerprintManagerInteractorTest { private val flow: FingerprintFlow = Default private val maxFingerprints = 5 private val currUser = MutableStateFlow(0) + private lateinit var fingerprintEnrollRepo: FingerprintEnrollmentRepository private val userRepo = object : UserRepo { override val currentUser: Flow = currUser + + override fun updateUser(user: Int) { + currUser.update { user } + } } @Before @@ -133,17 +139,18 @@ class FingerprintManagerInteractorTest { } val settingsRepository = FingerprintSettingsRepositoryImpl(maxFingerprints) - val fingerprintEnrollmentRepository = + fingerprintEnrollRepo = FingerprintEnrollmentRepositoryImpl( fingerprintManager, userRepo, settingsRepository, backgroundDispatcher, backgroundScope, + fingerprintSensorRepository, ) enrolledFingerprintsInteractorUnderTest = - EnrolledFingerprintsInteractorImpl(fingerprintManager, userId) + EnrolledFingerprintsInteractorImpl(fingerprintEnrollRepo) generateChallengeInteractorUnderTest = GenerateChallengeInteractorImpl(fingerprintManager, userId, gateKeeperPasswordProvider) removeFingerprintsInteractorUnderTest = @@ -153,7 +160,7 @@ class FingerprintManagerInteractorTest { authenticateInteractorImplUnderTest = AuthenticateInteractorImpl(fingerprintManager, userId) canEnrollFingerprintsInteractorUnderTest = - CanEnrollFingerprintsInteractorImpl(fingerprintEnrollmentRepository) + CanEnrollFingerprintsInteractorImpl(fingerprintEnrollRepo) enrollInteractorUnderTest = EnrollFingerprintInteractorImpl(userId, fingerprintManager, flow) } @@ -163,9 +170,16 @@ class FingerprintManagerInteractorTest { testScope.runTest { whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(emptyList()) - val emptyFingerprintList: List = emptyList() - assertThat(enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last()) - .isEqualTo(emptyFingerprintList) + var list: List? = null + val job = + testScope.launch { + enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.collect { list = it } + } + + runCurrent() + job.cancelAndJoin() + + assertThat(list!!.isEmpty()) } @Test @@ -174,10 +188,19 @@ class FingerprintManagerInteractorTest { val expected = Fingerprint("Finger 1,", 2, 3L) val fingerprintList: List = listOf(expected) whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) + // This causes the enrolled fingerprints to be updated + + var list: List? = null + val job = + testScope.launch { + enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.collect { list = it } + } + + runCurrent() + job.cancelAndJoin() - val list = enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last() assertThat(list!!.size).isEqualTo(fingerprintList.size) - val actual = list[0] + val actual = list!![0] assertThat(actual.name).isEqualTo(expected.name) assertThat(actual.fingerId).isEqualTo(expected.biometricId) assertThat(actual.deviceId).isEqualTo(expected.deviceId) @@ -220,11 +243,7 @@ class FingerprintManagerInteractorTest { whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList) var result: Boolean? = null - val job = - testScope.launch { - canEnrollFingerprintsInteractorUnderTest.canEnrollFingerprints.collect { result = it } - } - + val job = testScope.launch { fingerprintEnrollRepo.canEnrollUser.collect { result = it } } runCurrent() job.cancelAndJoin() diff --git a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModelTest.kt b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModelTest.kt index f59d1fcb820..a9ab5899e75 100644 --- a/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModelTest.kt +++ b/tests/unit/src/com/android/settings/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollConfirmationViewModelTest.kt @@ -112,7 +112,7 @@ class FingerprintEnrollConfirmationViewModelTest { .toFingerprintSensor() fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf() - fakeFingerprintManagerInteractor.enrollableFingerprints = 5 + fakeFingerprintManagerInteractor.setMaxEnrollableFingerprints(5) var canEnrollFingerprints: Boolean = false val job = launch {