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 {