Snap for 12420652 from b5aa5265ee to 25Q1-release

Change-Id: Ie68b8ebfecf31acf30ebc4b0f4036acc6e6ae751
This commit is contained in:
Android Build Coastguard Worker
2024-09-26 23:22:32 +00:00
41 changed files with 765 additions and 140 deletions

View File

@@ -130,6 +130,7 @@ android_library {
"ims-common",
],
flags_packages: [
"aconfig_settings_flags",
"android.app.flags-aconfig",
],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.domain.interactor
import com.android.settings.biometrics.fingerprint2.data.repository.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)
}

View File

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

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.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)
}

View File

@@ -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. */

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,7 +112,7 @@ class FingerprintEnrollConfirmationViewModelTest {
.toFingerprintSensor()
fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()
fakeFingerprintManagerInteractor.enrollableFingerprints = 5
fakeFingerprintManagerInteractor.setMaxEnrollableFingerprints(5)
var canEnrollFingerprints: Boolean = false
val job = launch {