Snap for 12420652 from b5aa5265ee to 25Q1-release
Change-Id: Ie68b8ebfecf31acf30ebc4b0f4036acc6e6ae751
This commit is contained in:
@@ -130,6 +130,7 @@ android_library {
|
||||
"ims-common",
|
||||
],
|
||||
flags_packages: [
|
||||
"aconfig_settings_flags",
|
||||
"android.app.flags-aconfig",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.USER_INITIALIZE"/>
|
||||
<action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
|
||||
<action android:name="com.google.android.setupwizard.SETUP_WIZARD_FINISHED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="true"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:padding="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
android:id="@+id/sfps_enrollment_finish_content_layout"
|
||||
android:layout_width="@dimen/sfps_enrollment_finished_icon_max_size"
|
||||
android:layout_height="@dimen/sfps_enrollment_finished_icon_max_size"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="@dimen/sfps_enroll_finish_icon_margin_top"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:layout_gravity="center">
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
<dimen name="sfps_progress_bar_translate_y">2dp</dimen>
|
||||
<dimen name="sfps_lottie_translate_x">12dp</dimen>
|
||||
<dimen name="sfps_lottie_translate_y">12dp</dimen>
|
||||
<dimen name="sfps_enroll_finish_icon_margin_top">-24dp</dimen>
|
||||
<dimen name="udfps_lottie_translate_y">0dp</dimen>
|
||||
<dimen name="udfps_lottie_padding_top">20dp</dimen>
|
||||
|
||||
|
||||
@@ -9585,6 +9585,9 @@
|
||||
<!-- Modes: Hint for the EditText for editing a mode's name [CHAR LIMIT=30] -->
|
||||
<string name="zen_mode_edit_name_hint">Mode name</string>
|
||||
|
||||
<!-- Modes: Text shown above the list of icons in the mode editor. [CHAR LIMIT=40] -->
|
||||
<string name="zen_mode_edit_choose_icon_title">Choose an icon</string>
|
||||
|
||||
<!-- Modes: Trigger title for modes of type SCHEDULE_CALENDAR. [CHAR LIMIT=30] -->
|
||||
<string name="zen_mode_trigger_title_schedule_calendar">Calendar events</string>
|
||||
<!-- Modes: Trigger title for modes of type BEDTIME. [CHAR LIMIT=30] -->
|
||||
|
||||
@@ -33,13 +33,15 @@
|
||||
android:key="name"
|
||||
android:layout="@layout/modes_edit_name" />
|
||||
|
||||
<com.android.settings.applications.SpacePreference
|
||||
android:layout_height="32dp" />
|
||||
<PreferenceCategory
|
||||
android:title="@string/zen_mode_edit_choose_icon_title"
|
||||
android:key="modes_filters">
|
||||
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="icon_list"
|
||||
android:selectable="false"
|
||||
android:layout="@layout/modes_icon_list"/>
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="icon_list"
|
||||
android:selectable="false"
|
||||
android:layout="@layout/modes_icon_list" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="done"
|
||||
|
||||
@@ -39,6 +39,7 @@ import android.util.Log;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
|
||||
import com.android.settings.core.instrumentation.ElapsedTimeUtils;
|
||||
import com.android.settings.homepage.DeepLinkHomepageActivity;
|
||||
import com.android.settings.search.SearchStateReceiver;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
@@ -69,6 +70,7 @@ public class SettingsInitialize extends BroadcastReceiver {
|
||||
webviewSettingSetup(context, pm, userInfo);
|
||||
ThreadUtils.postOnBackgroundThread(() -> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<List<FingerprintData>?>
|
||||
|
||||
/** Indicates the maximum fingerprints that are enrollable * */
|
||||
val maxFingerprintsEnrollable: Flow<Int>
|
||||
|
||||
/** Indicates if a user can enroll another fingerprint */
|
||||
val canEnrollUser: Flow<Boolean>
|
||||
|
||||
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<Int?> =
|
||||
callbackFlow {
|
||||
val callback =
|
||||
@@ -72,27 +85,34 @@ class FingerprintEnrollmentRepositoryImpl(
|
||||
override val currentEnrollments: Flow<List<FingerprintData>> =
|
||||
userRepo.currentUser
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { currentUser ->
|
||||
enrollmentChangedFlow.map { enrollmentChanged ->
|
||||
if (enrollmentChanged == null || enrollmentChanged == currentUser) {
|
||||
fingerprintManager
|
||||
.getEnrolledFingerprints(currentUser)
|
||||
?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }
|
||||
?.toList()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
.combine(enrollmentChangedFlow) { currentUser, _ -> getFingerprintsForUser(currentUser) }
|
||||
.filterNotNull()
|
||||
.flowOn(backgroundDispatcher)
|
||||
|
||||
override val canEnrollUser: Flow<Boolean> =
|
||||
currentEnrollments.map {
|
||||
it?.size?.let { it < settingsRepository.maxEnrollableFingerprints() } ?: false
|
||||
override val maxFingerprintsEnrollable: Flow<Int> =
|
||||
shouldUseSettingsMaxFingerprints.combine(sensorRepo.fingerprintSensor) {
|
||||
shouldUseSettings,
|
||||
sensor ->
|
||||
if (shouldUseSettings) {
|
||||
settingsRepository.maxEnrollableFingerprints()
|
||||
} else {
|
||||
sensor.maxEnrollmentsPerUser
|
||||
}
|
||||
}
|
||||
|
||||
override fun maxFingerprintsEnrollable(): Int {
|
||||
return settingsRepository.maxEnrollableFingerprints()
|
||||
override val canEnrollUser: Flow<Boolean> =
|
||||
currentEnrollments.combine(maxFingerprintsEnrollable) { enrollments, maxFingerprints ->
|
||||
enrollments.size < maxFingerprints
|
||||
}
|
||||
|
||||
override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) {
|
||||
_shouldUseSettingsMaxFingerprints.update { useSettings }
|
||||
}
|
||||
|
||||
private fun getFingerprintsForUser(userId: Int): List<FingerprintData>? {
|
||||
return fingerprintManager
|
||||
.getEnrolledFingerprints(userId)
|
||||
?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }
|
||||
?.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Int>
|
||||
|
||||
/**
|
||||
* Updates the current user.
|
||||
*/
|
||||
fun updateUser(user: Int)
|
||||
}
|
||||
|
||||
class UserRepoImpl(val currUser: Int): UserRepo {
|
||||
override val currentUser: Flow<Int> = 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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean> = fingerprintEnrollmentRepository.canEnrollUser
|
||||
/** Indicates the maximum fingerprints enrollable for a given user */
|
||||
override fun maxFingerprintsEnrollable(): Int {
|
||||
return fingerprintEnrollmentRepository.maxFingerprintsEnrollable()
|
||||
override val maxFingerprintsEnrollable: Flow<Int> =
|
||||
fingerprintEnrollmentRepository.maxFingerprintsEnrollable
|
||||
|
||||
override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) {
|
||||
fingerprintEnrollmentRepository.setShouldUseSettingsMaxFingerprints(useSettings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<FingerprintData>?> = flow {
|
||||
emit(
|
||||
fingerprintManager
|
||||
.getEnrolledFingerprints(userId)
|
||||
?.map { (FingerprintData(it.name.toString(), it.biometricId, it.deviceId)) }
|
||||
?.toList()
|
||||
)
|
||||
}
|
||||
override val enrolledFingerprints: Flow<List<FingerprintData>?> =
|
||||
fingerprintEnrollmentRepository.currentEnrollments
|
||||
}
|
||||
|
||||
@@ -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<Int> = userRepo.currentUser
|
||||
|
||||
override fun updateUser(user: Int) = userRepo.updateUser(user)
|
||||
}
|
||||
@@ -23,5 +23,17 @@ interface CanEnrollFingerprintsInteractor {
|
||||
/** Returns true if a user can enroll a fingerprint false otherwise. */
|
||||
val canEnrollFingerprints: Flow<Boolean>
|
||||
/** Indicates the maximum fingerprints enrollable for a given user */
|
||||
fun maxFingerprintsEnrollable(): Int
|
||||
val maxFingerprintsEnrollable: Flow<Int>
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
@@ -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<Int>
|
||||
|
||||
/**
|
||||
* Updates the current user.
|
||||
*/
|
||||
fun updateUser(user: Int)
|
||||
}
|
||||
@@ -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<Pair<Boolean, Int>> =
|
||||
_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. */
|
||||
|
||||
@@ -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<BluetoothDevice> 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
133
src/com/android/settings/bluetooth/ProgressDialogFragment.java
Normal file
133
src/com/android/settings/bluetooth/ProgressDialogFragment.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Int>> =
|
||||
@@ -172,6 +163,57 @@ class SubscriptionRepository(private val context: Context) {
|
||||
flowOf(null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var SharedSubscriptionsChangedFlow: Flow<Unit>
|
||||
|
||||
private fun getSharedSubscriptionsChangedFlow(context: Context): Flow<Unit> {
|
||||
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?
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Boolean> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FingerprintData> = mutableListOf()
|
||||
var challengeToGenerate: Pair<Long, ByteArray> = Pair(-1L, byteArrayOf())
|
||||
var authenticateAttempt = FingerprintAuthAttemptModel.Success(1)
|
||||
@@ -82,13 +86,13 @@ class FakeFingerprintManagerInteractor :
|
||||
override val enrolledFingerprints: Flow<List<FingerprintData>> = flow {
|
||||
emit(enrolledFingerprintsInternal)
|
||||
}
|
||||
override val canEnrollFingerprints: Flow<Boolean> = flow {
|
||||
emit(enrolledFingerprintsInternal.size < enrollableFingerprints)
|
||||
override val canEnrollFingerprints: Flow<Boolean> = enrollableFingerprints.transform {
|
||||
emit(enrolledFingerprintsInternal.size < it)
|
||||
}
|
||||
|
||||
override fun maxFingerprintsEnrollable(): Int {
|
||||
return enrollableFingerprints
|
||||
}
|
||||
override val maxFingerprintsEnrollable: Flow<Int> = enrollableFingerprints.asStateFlow()
|
||||
|
||||
override fun setShouldUseSettingsMaxFingerprints(useSettings: Boolean) {}
|
||||
|
||||
override val sensorPropertiesInternal: Flow<FingerprintSensor?> = flow { emit(sensorProp) }
|
||||
override val hasSideFps: Flow<Boolean> =
|
||||
@@ -110,4 +114,7 @@ class FakeFingerprintManagerInteractor :
|
||||
}
|
||||
}
|
||||
|
||||
fun setMaxEnrollableFingerprints(fingerprints: Int) {
|
||||
enrollableFingerprints.update { fingerprints }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,15 @@ class MobileNetworkSwitchControllerTest {
|
||||
on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false)
|
||||
}
|
||||
|
||||
private val mockSubscriptionActivationRepository = mock<SubscriptionActivationRepository> {
|
||||
on { isActivationChangeableFlow() } doReturn flowOf(true)
|
||||
}
|
||||
|
||||
private val controller = MobileNetworkSwitchController(
|
||||
context = context,
|
||||
preferenceKey = TEST_KEY,
|
||||
subscriptionRepository = mockSubscriptionRepository,
|
||||
subscriptionActivationRepository = mockSubscriptionActivationRepository,
|
||||
).apply { init(SUB_ID) }
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Int> = 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<Fingerprint> = emptyList()
|
||||
assertThat(enrolledFingerprintsInteractorUnderTest.enrolledFingerprints.last())
|
||||
.isEqualTo(emptyFingerprintList)
|
||||
var list: List<FingerprintData>? = 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<Fingerprint> = listOf(expected)
|
||||
whenever(fingerprintManager.getEnrolledFingerprints(anyInt())).thenReturn(fingerprintList)
|
||||
// This causes the enrolled fingerprints to be updated
|
||||
|
||||
var list: List<FingerprintData>? = 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()
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class FingerprintEnrollConfirmationViewModelTest {
|
||||
.toFingerprintSensor()
|
||||
|
||||
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
|
||||
fakeFingerprintManagerInteractor.enrollableFingerprints = 5
|
||||
fakeFingerprintManagerInteractor.setMaxEnrollableFingerprints(5)
|
||||
|
||||
var canEnrollFingerprints: Boolean = false
|
||||
val job = launch {
|
||||
|
||||
Reference in New Issue
Block a user