diff --git a/res/drawable/ic_head_tracking.xml b/res/drawable/ic_head_tracking.xml new file mode 100644 index 00000000000..d4a44fd9858 --- /dev/null +++ b/res/drawable/ic_head_tracking.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_spatial_audio.xml b/res/drawable/ic_spatial_audio.xml new file mode 100644 index 00000000000..0ee609ab79f --- /dev/null +++ b/res/drawable/ic_spatial_audio.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/drawable/ic_spatial_audio_off.xml b/res/drawable/ic_spatial_audio_off.xml new file mode 100644 index 00000000000..c7d3272b380 --- /dev/null +++ b/res/drawable/ic_spatial_audio_off.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/layout/preference_circular_icons.xml b/res/layout/preference_circular_icons.xml index ae981b2c562..863d2288513 100644 --- a/res/layout/preference_circular_icons.xml +++ b/res/layout/preference_circular_icons.xml @@ -61,6 +61,7 @@ 2 - - @*android:drawable/ic_zen_mode_type_bedtime - @*android:drawable/ic_zen_mode_type_driving - @*android:drawable/ic_zen_mode_type_immersive + + @*android:drawable/ic_zen_mode_icon_work + @*android:drawable/ic_zen_mode_icon_classical_building + @*android:drawable/ic_zen_mode_icon_apartment_building + @*android:drawable/ic_zen_mode_icon_speech_bubble + @*android:drawable/ic_zen_mode_icon_group_of_people + @*android:drawable/ic_zen_mode_icon_lightbulb @*android:drawable/ic_zen_mode_type_schedule_calendar - @*android:drawable/ic_zen_mode_type_schedule_time - @*android:drawable/ic_zen_mode_icon_beach - @*android:drawable/ic_zen_mode_icon_camping - @*android:drawable/ic_zen_mode_type_theater - @*android:drawable/ic_zen_mode_icon_gaming + + @*android:drawable/ic_zen_mode_icon_running + @*android:drawable/ic_zen_mode_icon_golf @*android:drawable/ic_zen_mode_icon_gym - @*android:drawable/ic_zen_mode_icon_ball_sports - @*android:drawable/ic_zen_mode_icon_martial_arts @*android:drawable/ic_zen_mode_icon_swimming @*android:drawable/ic_zen_mode_icon_hiking - @*android:drawable/ic_zen_mode_icon_golf + @*android:drawable/ic_zen_mode_icon_ball_sports + @*android:drawable/ic_zen_mode_icon_martial_arts + + @*android:drawable/ic_zen_mode_icon_gaming + @*android:drawable/ic_zen_mode_icon_palette + @*android:drawable/ic_zen_mode_icon_snowflake + @*android:drawable/ic_zen_mode_icon_beach @*android:drawable/ic_zen_mode_icon_workshop - @*android:drawable/ic_zen_mode_icon_work - @*android:drawable/ic_zen_mode_type_other - @*android:drawable/ic_zen_mode_type_unknown - @*android:drawable/ic_zen_mode_type_managed + @*android:drawable/ic_zen_mode_icon_camping + @*android:drawable/ic_zen_mode_type_theater + @*android:drawable/ic_zen_mode_icon_book + + @*android:drawable/ic_zen_mode_type_unknown + @*android:drawable/ic_zen_mode_type_immersive + @*android:drawable/ic_zen_mode_icon_headphones + @*android:drawable/ic_zen_mode_icon_tv + + @*android:drawable/ic_zen_mode_icon_train + @*android:drawable/ic_zen_mode_type_driving + @*android:drawable/ic_zen_mode_icon_croissant + @*android:drawable/ic_zen_mode_icon_fork_and_knife + @*android:drawable/ic_zen_mode_icon_shopping_cart + @*android:drawable/ic_zen_mode_icon_child + @*android:drawable/ic_zen_mode_icon_rabbit + @*android:drawable/ic_zen_mode_icon_animal_paw + + @*android:drawable/ic_zen_mode_type_managed + @*android:drawable/ic_zen_mode_type_other + @*android:drawable/ic_zen_mode_icon_heart + @*android:drawable/ic_zen_mode_icon_house + @*android:drawable/ic_zen_mode_type_bedtime + @*android:drawable/ic_zen_mode_type_schedule_time - - Half-moon - Car - Person\'s mind + + Briefcase + Classical building + Apartment building + Speech bubble + Group of people + Lightbulb Calendar - Clock - Beach umbrella - Tent - Film reel - Game controller + + Person running + Golf Gym dumbbell - Person throwing ball - Person kicking Swimming Person hiking - Golf + Person throwing ball + Person kicking + + Game controller + Artist color palette + Snowflake + Beach umbrella Workshop tools - Briefcase - Star + Tent + Film reel + Book + Lotus flower + Person\'s mind + Headphones + TV + + Train + Car + Croissant + Fork and knife + Shopping cart + Child + Rabbit + Animal paw + Supervisor + Star + Heart + House + Half-moon + Clock diff --git a/res/values/strings.xml b/res/values/strings.xml index 6c018c23ec9..4b30dc19abe 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7946,6 +7946,18 @@ Connected devices settings + + Spatial Audio + + + Off + + + Off + + + Off + {count, plural, diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java index ab7a7146329..711d7943ffa 100644 --- a/src/com/android/settings/MainClear.java +++ b/src/com/android/settings/MainClear.java @@ -183,13 +183,16 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis if (requestCode == KEYGUARD_REQUEST) { final int userId = getActivity().getUserId(); - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, - userId)) { + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + false /* biometricsAuthenticationRequested */, + userId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRICS_REQUEST, userId, false /* hideBackground */); return; + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + return; } } diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index badcb63791c..3646938e1f8 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -199,6 +199,15 @@ public final class Utils extends com.android.settingslib.Utils { return ActivityManager.isUserAMonkey(); } + /** + * Enum for returning biometric status. + * {@link OK} no error detected when requesting mandatory biometrics authentication + * {@link NOT_ACTIVE} mandatory biometrics is not active + * {@link LOCKOUT} biometric sensors are in lockout mode + * {@link ERROR} corresponds to other errors + */ + public enum BiometricStatus {OK, NOT_ACTIVE, LOCKOUT, ERROR} + /** * Returns whether the device is voice-capable (meaning, it is also a phone). */ @@ -1489,34 +1498,41 @@ public final class Utils extends com.android.settingslib.Utils { /** * Request biometric authentication if all requirements for mandatory biometrics is satisfied. * - * @param context of the corresponding activity/fragment - * @param biometricsSuccessfullyAuthenticated if the user has already authenticated using - * biometrics - * @param biometricsAuthenticationRequested if the activity/fragment has already requested for - * biometric prompt - * @param userId user id for the authentication request - * @return true if all requirements for mandatory biometrics is satisfied + * @param context of the corresponding activity/fragment + * @param biometricsAuthenticationRequested if the activity/fragment has already requested for + * biometric prompt + * @param userId user id for the authentication request + * @return biometric status when mandatory biometrics authentication is requested */ - public static boolean requestBiometricAuthenticationForMandatoryBiometrics( + public static BiometricStatus requestBiometricAuthenticationForMandatoryBiometrics( @NonNull Context context, - boolean biometricsSuccessfullyAuthenticated, boolean biometricsAuthenticationRequested, int userId) { final BiometricManager biometricManager = context.getSystemService(BiometricManager.class); if (biometricManager == null) { Log.e(TAG, "Biometric Manager is null."); - return false; + return BiometricStatus.NOT_ACTIVE; } final int status = biometricManager.canAuthenticate(userId, BiometricManager.Authenticators.MANDATORY_BIOMETRICS); - return android.hardware.biometrics.Flags.mandatoryBiometrics() - && status == BiometricManager.BIOMETRIC_SUCCESS - && !biometricsSuccessfullyAuthenticated - && !biometricsAuthenticationRequested; + if (android.hardware.biometrics.Flags.mandatoryBiometrics() + && !biometricsAuthenticationRequested) { + switch(status) { + case BiometricManager.BIOMETRIC_SUCCESS: + return BiometricStatus.OK; + case BiometricManager.BIOMETRIC_ERROR_LOCKOUT: + return BiometricStatus.LOCKOUT; + case BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE: + return BiometricStatus.NOT_ACTIVE; + default: + return BiometricStatus.ERROR; + } + } + return BiometricStatus.NOT_ACTIVE; } /** * Launch biometric prompt for mandatory biometrics. Call - * {@link #requestBiometricAuthenticationForMandatoryBiometrics(Context, boolean, boolean, int)} + * {@link #requestBiometricAuthenticationForMandatoryBiometrics(Context, boolean, int)} * to check if all requirements for mandatory biometrics is satisfied * before launching biometric prompt. * diff --git a/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java index 833638bb2e0..818eb5e4bab 100644 --- a/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java +++ b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java @@ -110,7 +110,7 @@ public class KeyboardVibrationTogglePreferenceController extends TogglePreferenc @Override public int getAvailabilityStatus() { if (mContext.getResources().getBoolean( - com.android.internal.R.bool.config_keyboardVibrationSettingsSupported)) { + com.android.internal.R.bool.config_keyboardVibrationSettingsSupported)) { return AVAILABLE; } return UNSUPPORTED_ON_DEVICE; @@ -128,15 +128,9 @@ public class KeyboardVibrationTogglePreferenceController extends TogglePreferenc mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_KEYBOARD_VIBRATION_CHANGED, isChecked); if (success && isChecked) { - // Play the preview vibration effect when the toggle is on. - final VibrationAttributes touchAttrs = - VibrationPreferenceConfig.createPreviewVibrationAttributes( - VibrationAttributes.USAGE_TOUCH); - final VibrationAttributes keyboardAttrs = - new VibrationAttributes.Builder(touchAttrs) - .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) - .build(); - VibrationPreferenceConfig.playVibrationPreview(mVibrator, keyboardAttrs); + // Play the preview vibration effect for the IME feedback when the toggle is on. + VibrationPreferenceConfig.playVibrationPreview( + mVibrator, VibrationAttributes.USAGE_IME_FEEDBACK); } return true; } diff --git a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java index a3048622fb0..ec1fab1af72 100644 --- a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java +++ b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java @@ -68,19 +68,8 @@ public abstract class VibrationPreferenceConfig { /** Play a vibration effect with intensity just selected by the user. */ public static void playVibrationPreview(Vibrator vibrator, @VibrationAttributes.Usage int vibrationUsage) { - playVibrationPreview(vibrator, createPreviewVibrationAttributes(vibrationUsage)); - } - - /** - * Play a vibration effect with intensity just selected by the user. - * - * @param vibrator The {@link Vibrator} used to play the vibration. - * @param vibrationAttributes The {@link VibrationAttributes} to indicate the - * vibration information. - */ - public static void playVibrationPreview(Vibrator vibrator, - VibrationAttributes vibrationAttributes) { - vibrator.vibrate(PREVIEW_VIBRATION_EFFECT, vibrationAttributes); + vibrator.vibrate(PREVIEW_VIBRATION_EFFECT, + createPreviewVibrationAttributes(vibrationUsage)); } public VibrationPreferenceConfig(Context context, String settingKey, diff --git a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java index 1e7ca1e5802..b40f62f4169 100644 --- a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java +++ b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java @@ -33,6 +33,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.internal.app.LocaleHelper; @@ -62,6 +63,7 @@ public class AppLocaleDetails extends SettingsPreferenceFragment { private LayoutPreference mPrefOfDescription; private Preference mPrefOfDisclaimer; private ApplicationInfo mApplicationInfo; + @Nullable private String mParentLocale; /** * Create a instance of AppLocaleDetails. @@ -111,6 +113,12 @@ public class AppLocaleDetails extends SettingsPreferenceFragment { public void onResume() { super.onResume(); refreshUi(); + final Activity activity = getActivity(); + if (mParentLocale != null) { + activity.setTitle(mParentLocale); + } else { + activity.setTitle(R.string.app_locale_picker_title); + } } private void refreshUi() { @@ -215,4 +223,8 @@ public class AppLocaleDetails extends SettingsPreferenceFragment { return LocaleHelper.getDisplayName(appLocale.stripExtensions(), appLocale, true); } } + + public void setParentLocale(@Nullable String localeName) { + mParentLocale = localeName; + } } diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index 11194ce92dc..43b5da28044 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -75,14 +75,11 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @VisibleForTesting static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle"; private static final String BIOMETRICS_AUTH_REQUESTED = "biometrics_auth_requested"; - private static final String BIOMETRICS_AUTHENTICATED_SUCCESSFULLY = - "biometrics_authenticated_successfully"; protected int mUserId; protected long mGkPwHandle; private boolean mConfirmCredential; private boolean mBiometricsAuthenticationRequested; - private boolean mBiometricsSuccessfullyAuthenticated; @Nullable private FaceManager mFaceManager; @Nullable private FingerprintManager mFingerprintManager; // Do not finish() if choosing/confirming credential, showing fp/face settings, or launching @@ -120,9 +117,6 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent()); } - mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( - BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); - if (savedInstanceState != null) { mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL); mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY); @@ -135,21 +129,12 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { } mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( BIOMETRICS_AUTH_REQUESTED); - mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( - BIOMETRICS_AUTHENTICATED_SUCCESSFULLY); } if (mGkPwHandle == 0L && !mConfirmCredential) { mConfirmCredential = true; launchChooseOrConfirmLock(); - } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics( - getActivity(), mBiometricsSuccessfullyAuthenticated, - mBiometricsAuthenticationRequested, mUserId)) { - mBiometricsAuthenticationRequested = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST, - mUserId, true /* hideBackground */); } - updateUnlockPhonePreferenceSummary(); final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey()); @@ -161,13 +146,6 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @Override public void onResume() { super.onResume(); - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), - mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested, mUserId) - && mGkPwHandle != 0L) { - mBiometricsAuthenticationRequested = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST, - mUserId, true /* hideBackground */); - } if (!mConfirmCredential) { mDoNotFinishActivity = false; } @@ -204,9 +182,6 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); - extras.putBoolean( - BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, - mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -236,9 +211,6 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { final Bundle extras = preference.getExtras(); extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); - extras.putBoolean( - BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, - mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -323,8 +295,6 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { } outState.putBoolean(BIOMETRICS_AUTH_REQUESTED, mBiometricsAuthenticationRequested); - outState.putBoolean(BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, - mBiometricsSuccessfullyAuthenticated); } @Override @@ -342,6 +312,20 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { com.google.android.setupdesign.R.anim.sud_slide_next_out); retryPreferenceKey(mRetryPreferenceKey, mRetryPreferenceExtra); } + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics( + getActivity(), + mBiometricsAuthenticationRequested, + mUserId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, + mUserId, true /* hideBackground */); + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); + return; + } } else { Log.d(getLogTag(), "Data null or GK PW missing."); finish(); @@ -354,9 +338,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mRetryPreferenceExtra = null; } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { mBiometricsAuthenticationRequested = false; - if (resultCode == RESULT_OK) { - mBiometricsSuccessfullyAuthenticated = true; - } else { + if (resultCode != RESULT_OK) { finish(); } } diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java index bcd523142be..d42b570b30d 100644 --- a/src/com/android/settings/biometrics/face/FaceSettings.java +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -23,7 +23,6 @@ import static com.android.settings.Utils.isPrivateProfile; import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.ENROLL_REQUEST; -import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; @@ -98,7 +97,6 @@ public class FaceSettings extends DashboardFragment { private boolean mConfirmingPassword; private boolean mBiometricsAuthenticationRequested; - private boolean mBiometricsSuccessfullyAuthenticated; private final FaceSettingsRemoveButtonPreferenceController.Listener mRemovalListener = () -> { @@ -150,8 +148,6 @@ public class FaceSettings extends DashboardFragment { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putByteArray(KEY_TOKEN, mToken); - outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, - mBiometricsSuccessfullyAuthenticated); } @Override @@ -171,8 +167,6 @@ public class FaceSettings extends DashboardFragment { mToken = getIntent().getByteArrayExtra(KEY_TOKEN); mSensorId = getIntent().getIntExtra(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, -1); mChallenge = getIntent().getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, 0L); - mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( - EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mUserId = getActivity().getIntent().getIntExtra( Intent.EXTRA_USER_ID, UserHandle.myUserId()); @@ -241,8 +235,6 @@ public class FaceSettings extends DashboardFragment { if (savedInstanceState != null) { mToken = savedInstanceState.getByteArray(KEY_TOKEN); - mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( - KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); } } @@ -288,12 +280,6 @@ public class FaceSettings extends DashboardFragment { Log.e(TAG, "Password not set"); finish(); } - } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), - mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested, - mUserId)) { - mBiometricsAuthenticationRequested = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST, - mUserId, true /* hideBackground */); } else { mAttentionController.setToken(mToken); mEnrollController.setToken(mToken); @@ -330,6 +316,17 @@ public class FaceSettings extends DashboardFragment { final boolean hasEnrolled = mFaceManager.hasEnrolledTemplates(mUserId); mEnrollButton.setVisible(!hasEnrolled); mRemoveButton.setVisible(hasEnrolled); + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsAuthenticationRequested, + mUserId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, + mUserId, true /* hideBackground */); + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); + } } } else if (requestCode == ENROLL_REQUEST) { if (resultCode == RESULT_TIMEOUT) { @@ -338,9 +335,7 @@ public class FaceSettings extends DashboardFragment { } } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { mBiometricsAuthenticationRequested = false; - if (resultCode == RESULT_OK) { - mBiometricsSuccessfullyAuthenticated = true; - } else { + if (resultCode != RESULT_OK) { finish(); } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index 526ae8f6ed0..125691fbf1c 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -239,8 +239,6 @@ public class FingerprintSettings extends SubSettings { "security_settings_fingerprint_footer"; private static final String KEY_BIOMETRICS_AUTHENTICATION_REQUESTED = "biometrics_authentication_requested"; - private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = - "biometrics_successfully_authenticated"; private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000; private static final int MSG_FINGER_AUTH_SUCCESS = 1001; @@ -276,7 +274,6 @@ public class FingerprintSettings extends SubSettings { private byte[] mToken; private boolean mLaunchedConfirm; private boolean mBiometricsAuthenticationRequested; - private boolean mBiometricsSuccessfullyAuthenticated; private boolean mHasFirstEnrolled = true; private Drawable mHighlightDrawable; private int mUserId; @@ -451,8 +448,6 @@ public class FingerprintSettings extends SubSettings { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); mChallenge = activity.getIntent() .getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L); - mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( - BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mAuthenticateSidecar = (FingerprintAuthenticateSidecar) getFragmentManager().findFragmentByTag(TAG_AUTHENTICATE_SIDECAR); @@ -494,8 +489,6 @@ public class FingerprintSettings extends SubSettings { mIsEnrolling = savedInstanceState.getBoolean(KEY_IS_ENROLLING, mIsEnrolling); mHasFirstEnrolled = savedInstanceState.getBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); - mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( - KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( KEY_BIOMETRICS_AUTHENTICATION_REQUESTED); } @@ -506,12 +499,6 @@ public class FingerprintSettings extends SubSettings { if (mToken == null) { mLaunchedConfirm = true; launchChooseOrConfirmLock(); - } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), - mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested, - mUserId)) { - mBiometricsAuthenticationRequested = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST, - mUserId, true /* hideBackground */); } else if (!mHasFirstEnrolled) { mIsEnrolling = true; addFirstFingerprint(null); @@ -801,14 +788,6 @@ public class FingerprintSettings extends SubSettings { mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider() .getUdfpsEnrollCalibrator(getActivity().getApplicationContext(), null, null); - - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), - mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested, - mUserId)) { - mBiometricsAuthenticationRequested = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, - BIOMETRIC_AUTH_REQUEST, mUserId, true /* hideBackground */); - } } private void updatePreferences() { @@ -858,8 +837,6 @@ public class FingerprintSettings extends SubSettings { outState.putBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); outState.putBoolean(KEY_BIOMETRICS_AUTHENTICATION_REQUESTED, mBiometricsAuthenticationRequested); - outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, - mBiometricsSuccessfullyAuthenticated); } @Override @@ -1023,6 +1000,18 @@ public class FingerprintSettings extends SubSettings { updateAddPreference(); }); } + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics( + getActivity(), + mBiometricsAuthenticationRequested, + mUserId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, + mUserId, true /* hideBackground */); + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); + } } else { Log.d(TAG, "Data null or GK PW missing"); finish(); @@ -1075,9 +1064,7 @@ public class FingerprintSettings extends SubSettings { updateAddPreference(); } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { mBiometricsAuthenticationRequested = false; - if (resultCode == RESULT_OK) { - mBiometricsSuccessfullyAuthenticated = true; - } else { + if (resultCode != RESULT_OK) { finish(); } } diff --git a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java index 0690186b972..442acd2dd3f 100644 --- a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java +++ b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java @@ -101,7 +101,8 @@ public class BlockingPrefWithSliceController extends BasePreferenceController im return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } - public void setSliceUri(Uri uri) { + /** Sets Slice uri for the preference. */ + public void setSliceUri(@Nullable Uri uri) { mUri = uri; mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> { Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type); diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java index 4ff71360a49..398edb6b991 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java @@ -39,8 +39,8 @@ import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; @@ -299,57 +299,14 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont + " profiles: " + mCachedDevice.getProfiles()); - AudioDeviceAttributes saDevice = null; - for (LocalBluetoothProfile profile : mCachedDevice.getProfiles()) { - // pick first enabled profile that is compatible with spatial audio - if (SA_PROFILES.contains(profile.getProfileId()) - && profile.isEnabled(mCachedDevice.getDevice())) { - switch (profile.getProfileId()) { - case BluetoothProfile.A2DP: - saDevice = - new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, - mCachedDevice.getAddress()); - break; - case BluetoothProfile.LE_AUDIO: - if (mAudioManager.getBluetoothAudioDeviceCategory( - mCachedDevice.getAddress()) - == AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) { - saDevice = - new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_BLE_SPEAKER, - mCachedDevice.getAddress()); - } else { - saDevice = - new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_BLE_HEADSET, - mCachedDevice.getAddress()); - } - - break; - case BluetoothProfile.HEARING_AID: - saDevice = - new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_HEARING_AID, - mCachedDevice.getAddress()); - break; - default: - Log.i( - TAG, - "unrecognized profile for spatial audio: " - + profile.getProfileId()); - break; - } - break; - } - } - mAudioDevice = null; + AudioDeviceAttributes saDevice = + BluetoothUtils.getAudioDeviceAttributesForSpatialAudio( + mCachedDevice, + mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress())); if (saDevice != null && mSpatializer.isAvailableForDevice(saDevice)) { mAudioDevice = saDevice; + } else { + mAudioDevice = null; } Log.d( diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index ccf38ed2835..bd762a1ef11 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -43,10 +43,12 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; +import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settings.connecteddevice.stylus.StylusDevicesController; import com.android.settings.core.SettingsUIDeviceConfig; import com.android.settings.dashboard.RestrictedDashboardFragment; @@ -60,9 +62,11 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment { public static final String KEY_DEVICE_ADDRESS = "device_address"; @@ -98,6 +102,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment @VisibleForTesting CachedBluetoothDevice mCachedDevice; BluetoothAdapter mBluetoothAdapter; + @VisibleForTesting + DeviceDetailsFragmentFormatter mFormatter; @Nullable InputDevice mInputDevice; @@ -214,18 +220,29 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment finish(); return; } - use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice, this); - use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager, this); - use(KeyboardSettingsPreferenceController.class).init(mCachedDevice); + getController( + AdvancedBluetoothDetailsHeaderController.class, + controller -> controller.init(mCachedDevice, this)); + getController( + LeAudioBluetoothDetailsHeaderController.class, + controller -> controller.init(mCachedDevice, mManager, this)); + getController( + KeyboardSettingsPreferenceController.class, + controller -> controller.init(mCachedDevice)); final BluetoothFeatureProvider featureProvider = FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider(); final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true); - use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled - ? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice()) - : null); + getController( + BlockingPrefWithSliceController.class, + controller -> + controller.setSliceUri( + sliceEnabled + ? featureProvider.getBluetoothDeviceSettingsUri( + mCachedDevice.getDevice()) + : null)); mManager.getEventManager().registerCallback(mBluetoothCallback); mBluetoothAdapter.addOnMetadataChangedListener( @@ -257,21 +274,35 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment } } mExtraControlUriLoaded |= controlUri != null; - final SlicePreferenceController slicePreferenceController = use( - SlicePreferenceController.class); - slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null); - slicePreferenceController.onStart(); - slicePreferenceController.displayPreference(getPreferenceScreen()); + + Uri finalControlUri = controlUri; + getController(SlicePreferenceController.class, controller -> { + controller.setSliceUri(sliceEnabled ? finalControlUri : null); + controller.onStart(); + controller.displayPreference(getPreferenceScreen()); + }); + // Temporarily fix the issue that the page will be automatically scrolled to a wrong // position when entering the page. This will make sure the bluetooth header is shown on top // of the page. - use(LeAudioBluetoothDetailsHeaderController.class).displayPreference( - getPreferenceScreen()); - use(AdvancedBluetoothDetailsHeaderController.class).displayPreference( - getPreferenceScreen()); - use(BluetoothDetailsHeaderController.class).displayPreference( - getPreferenceScreen()); + getController( + LeAudioBluetoothDetailsHeaderController.class, + controller -> controller.displayPreference(getPreferenceScreen())); + getController( + AdvancedBluetoothDetailsHeaderController.class, + controller -> controller.displayPreference(getPreferenceScreen())); + getController( + BluetoothDetailsHeaderController.class, + controller -> controller.displayPreference(getPreferenceScreen())); + } + + protected void getController(Class clazz, + Consumer action) { + T controller = use(clazz); + if (controller != null) { + action.accept(controller); + } } private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = @@ -308,6 +339,14 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment return view; } + @Override + public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + if (Flags.enableBluetoothDeviceDetailsPolish()) { + mFormatter.updateLayout(); + } + } + @Override public void onResume() { super.onResume(); @@ -358,8 +397,30 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment return super.onOptionsItemSelected(menuItem); } + @Override + protected void addPreferenceController(AbstractPreferenceController controller) { + if (Flags.enableBluetoothDeviceDetailsPolish()) { + List keys = mFormatter.getVisiblePreferenceKeysForMainPage(); + Lifecycle lifecycle = getSettingsLifecycle(); + if (keys == null || keys.contains(controller.getPreferenceKey())) { + super.addPreferenceController(controller); + } else if (controller instanceof LifecycleObserver) { + lifecycle.removeObserver((LifecycleObserver) controller); + } + } else { + super.addPreferenceController(controller); + } + } + @Override protected List createPreferenceControllers(Context context) { + if (Flags.enableBluetoothDeviceDetailsPolish()) { + mFormatter = + FeatureFactory.getFeatureFactory() + .getBluetoothFeatureProvider() + .getDeviceDetailsFragmentFormatter( + requireContext(), this, mBluetoothAdapter, mCachedDevice); + } ArrayList controllers = new ArrayList<>(); if (mCachedDevice != null) { diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java index 1751082a45f..be0f6f36b6c 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java @@ -16,15 +16,23 @@ package com.android.settings.bluetooth; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; +import android.media.AudioManager; import android.media.Spatializer; import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleCoroutineScope; import androidx.preference.Preference; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor; +import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository; import java.util.List; import java.util.Set; @@ -84,4 +92,26 @@ public interface BluetoothFeatureProvider { */ Set getInvisibleProfilePreferenceKeys( Context context, BluetoothDevice bluetoothDevice); + + /** Gets DeviceSettingRepository. */ + @NonNull + DeviceSettingRepository getDeviceSettingRepository( + @NonNull Context context, + @NonNull BluetoothAdapter bluetoothAdapter, + @NonNull LifecycleCoroutineScope scope); + + /** Gets spatial audio interactor. */ + @NonNull + SpatialAudioInteractor getSpatialAudioInteractor( + @NonNull Context context, + @NonNull AudioManager audioManager, + @NonNull LifecycleCoroutineScope scope); + + /** Gets device details fragment layout formatter. */ + @NonNull + DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter( + @NonNull Context context, + @NonNull SettingsPreferenceFragment fragment, + @NonNull BluetoothAdapter bluetoothAdapter, + @NonNull CachedBluetoothDevice cachedDevice); } diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java deleted file mode 100644 index 2d4ac496d49..00000000000 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2018 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.bluetooth.BluetoothDevice; -import android.content.ComponentName; -import android.content.Context; -import android.media.AudioManager; -import android.media.Spatializer; -import android.net.Uri; - -import androidx.preference.Preference; - -import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - -import java.util.List; -import java.util.Set; - -/** - * Impl of {@link BluetoothFeatureProvider} - */ -public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider { - - @Override - public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) { - final byte[] uriByte = bluetoothDevice.getMetadata( - BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI); - return uriByte == null ? null : Uri.parse(new String(uriByte)); - } - - @Override - public String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice) { - return BluetoothUtils.getControlUriMetaData(bluetoothDevice); - } - - @Override - public List getRelatedTools() { - return null; - } - - @Override - public Spatializer getSpatializer(Context context) { - AudioManager audioManager = context.getSystemService(AudioManager.class); - return audioManager.getSpatializer(); - } - - @Override - public List getBluetoothExtraOptions(Context context, - CachedBluetoothDevice device) { - return ImmutableList.of(); - } - - @Override - public Set getInvisibleProfilePreferenceKeys( - Context context, BluetoothDevice bluetoothDevice) { - return ImmutableSet.of(); - } -} diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt new file mode 100644 index 00000000000..3a549c6b2de --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 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.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.content.ComponentName +import android.content.Context +import android.media.AudioManager +import android.media.Spatializer +import android.net.Uri +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.preference.Preference +import com.android.settings.SettingsPreferenceFragment +import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor +import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl +import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter +import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository +import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl +import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet +import kotlinx.coroutines.Dispatchers + +/** Impl of [BluetoothFeatureProvider] */ +open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider { + override fun getBluetoothDeviceSettingsUri(bluetoothDevice: BluetoothDevice): Uri? { + val uriByte = bluetoothDevice.getMetadata(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI) + return uriByte?.let { Uri.parse(String(it)) } + } + + override fun getBluetoothDeviceControlUri(bluetoothDevice: BluetoothDevice): String? { + return BluetoothUtils.getControlUriMetaData(bluetoothDevice) + } + + override fun getRelatedTools(): List? { + return null + } + + override fun getSpatializer(context: Context): Spatializer? { + val audioManager = context.getSystemService(AudioManager::class.java) + return audioManager.spatializer + } + + override fun getBluetoothExtraOptions( + context: Context, + device: CachedBluetoothDevice + ): List? { + return ImmutableList.of() + } + + override fun getInvisibleProfilePreferenceKeys( + context: Context, + bluetoothDevice: BluetoothDevice + ): Set { + return ImmutableSet.of() + } + + override fun getDeviceSettingRepository( + context: Context, + bluetoothAdapter: BluetoothAdapter, + scope: LifecycleCoroutineScope + ): DeviceSettingRepository = + DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO) + + override fun getSpatialAudioInteractor( + context: Context, + audioManager: AudioManager, + scope: LifecycleCoroutineScope + ): SpatialAudioInteractor { + return SpatialAudioInteractorImpl( + context, audioManager, + SpatializerInteractor( + SpatializerRepositoryImpl( + audioManager.spatializer, + Dispatchers.IO + ) + ), scope, Dispatchers.IO) + } + + override fun getDeviceDetailsFragmentFormatter( + context: Context, + fragment: SettingsPreferenceFragment, + bluetoothAdapter: BluetoothAdapter, + cachedDevice: CachedBluetoothDevice + ): DeviceDetailsFragmentFormatter { + return DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice) + } +} diff --git a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt new file mode 100644 index 00000000000..6b72b53aa3f --- /dev/null +++ b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt @@ -0,0 +1,155 @@ +/* + * 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.domain.interactor + +import android.content.Context +import android.media.AudioManager +import android.util.Log +import com.android.settings.R +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Provides device setting for spatial audio. */ +interface SpatialAudioInteractor { + /** Gets device setting for spatial audio */ + fun getDeviceSetting( + cachedDevice: CachedBluetoothDevice, + ): Flow +} + +class SpatialAudioInteractorImpl( + private val context: Context, + private val audioManager: AudioManager, + private val spatializerInteractor: SpatializerInteractor, + private val coroutineScope: CoroutineScope, + private val backgroundCoroutineContext: CoroutineContext, +) : SpatialAudioInteractor { + private val spatialAudioOffToggle = + ToggleModel( + context.getString(R.string.spatial_audio_multi_toggle_off), + DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off)) + private val spatialAudioOnToggle = + ToggleModel( + context.getString(R.string.spatial_audio_multi_toggle_on), + DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio)) + private val headTrackingOnToggle = + ToggleModel( + context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on), + DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking)) + private val changes = MutableSharedFlow() + + override fun getDeviceSetting( + cachedDevice: CachedBluetoothDevice, + ): Flow = + changes + .onStart { emit(Unit) } + .map { getSpatialAudioDeviceSettingModel(cachedDevice) } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null) + + private suspend fun getSpatialAudioDeviceSettingModel( + cachedDevice: CachedBluetoothDevice, + ): DeviceSettingModel? { + // TODO(b/343317785): use audio repository instead of calling AudioManager directly. + Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}") + val attributes = + BluetoothUtils.getAudioDeviceAttributesForSpatialAudio( + cachedDevice, audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address)) + ?: run { + Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.") + return null + } + + Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.") + val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes) + if (!spatialAudioAvailable) { + Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}") + return null + } + val headTrackingAvailable = + spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes) + val toggles = + if (headTrackingAvailable) { + listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle) + } else { + listOf(spatialAudioOffToggle, spatialAudioOnToggle) + } + val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes) + val headTrackingEnabled = + spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes) + + val activeIndex = + when { + headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED + spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON + else -> INDEX_SPATIAL_AUDIO_OFF + } + Log.i( + TAG, + "Head tracking available: $headTrackingAvailable, " + + "spatial audio enabled: $spatialAudioEnabled, " + + "head tracking enabled: $headTrackingEnabled") + return DeviceSettingModel.MultiTogglePreference( + cachedDevice = cachedDevice, + id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE, + title = context.getString(R.string.spatial_audio_multi_toggle_title), + toggles = toggles, + isActive = spatialAudioEnabled, + state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex), + isAllowedChangingState = true, + updateState = { newState -> + coroutineScope.launch(backgroundCoroutineContext) { + Log.i(TAG, "Update spatial audio state: $newState") + when (newState.selectedIndex) { + INDEX_SPATIAL_AUDIO_OFF -> { + spatializerInteractor.setSpatialAudioEnabled(attributes, false) + } + INDEX_SPATIAL_AUDIO_ON -> { + spatializerInteractor.setSpatialAudioEnabled(attributes, true) + spatializerInteractor.setHeadTrackingEnabled(attributes, false) + } + INDEX_HEAD_TRACKING_ENABLED -> { + spatializerInteractor.setSpatialAudioEnabled(attributes, true) + spatializerInteractor.setHeadTrackingEnabled(attributes, true) + } + } + changes.emit(Unit) + } + }) + } + + companion object { + private const val TAG = "SpatialAudioInteractorImpl" + private const val INDEX_SPATIAL_AUDIO_OFF = 0 + private const val INDEX_SPATIAL_AUDIO_ON = 1 + private const val INDEX_HEAD_TRACKING_ENABLED = 2 + } +} diff --git a/src/com/android/settings/bluetooth/ui/composable/Icon.kt b/src/com/android/settings/bluetooth/ui/composable/Icon.kt new file mode 100644 index 00000000000..676bd14fcca --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/composable/Icon.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth.ui.composable + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.painterResource +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon + +@Composable +fun Icon( + icon: DeviceSettingIcon, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current, +) { + when (icon) { + is DeviceSettingIcon.BitmapIcon -> + androidx.compose.material3.Icon( + icon.bitmap.asImageBitmap(), + contentDescription = null, + modifier = modifier, + tint = LocalContentColor.current) + is DeviceSettingIcon.ResourceIcon -> + androidx.compose.material3.Icon( + painterResource(icon.resId), + contentDescription = null, + modifier = modifier, + tint = tint) + else -> {} + } +} diff --git a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt similarity index 80% rename from src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt rename to src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt index e4ca00d47a9..8fe3c255d34 100644 --- a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.bluetooth.ui +package com.android.settings.bluetooth.ui.composable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -51,7 +51,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -67,6 +66,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties import com.android.settings.R +import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.spa.framework.theme.SettingsDimension @@ -97,35 +97,29 @@ fun MultiTogglePreferenceGroup( Surface( modifier = Modifier.height(64.dp), shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surface - ) { - Button( - modifier = - Modifier.fillMaxSize().padding(8.dp).semantics { - role = Role.Switch - toggleableState = - if (preferenceModel.isActive) { - ToggleableState.On - } else { - ToggleableState.Off - } - contentDescription = preferenceModel.title - }, - onClick = { settingIdForPopUp = preferenceModel.id }, - shape = RoundedCornerShape(20.dp), - colors = getButtonColors(preferenceModel.isActive), - contentPadding = PaddingValues(0.dp) - ) { - Icon( - preferenceModel.toggles[preferenceModel.state.selectedIndex] - .icon - .asImageBitmap(), - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = LocalContentColor.current - ) + color = MaterialTheme.colorScheme.surface) { + Button( + modifier = + Modifier.fillMaxSize().padding(8.dp).semantics { + role = Role.Switch + toggleableState = + if (preferenceModel.isActive) { + ToggleableState.On + } else { + ToggleableState.Off + } + contentDescription = preferenceModel.title + }, + onClick = { settingIdForPopUp = preferenceModel.id }, + shape = RoundedCornerShape(20.dp), + colors = getButtonColors(preferenceModel.isActive), + contentPadding = PaddingValues(0.dp)) { + DeviceSettingComposeIcon( + preferenceModel.toggles[preferenceModel.state.selectedIndex] + .icon, + modifier = Modifier.size(24.dp)) + } } - } } Row { Text(text = preferenceModel.title, fontSize = 12.sp) } } @@ -173,8 +167,7 @@ private fun dialog( Icon( painterResource(id = R.drawable.ic_close), null, - tint = MaterialTheme.colorScheme.inverseSurface - ) + tint = MaterialTheme.colorScheme.inverseSurface) } Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) { dialogContent(multiTogglePreference) @@ -182,8 +175,7 @@ private fun dialog( } }, ) - } - ) + }) } @Composable @@ -208,9 +200,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP Modifier.fillMaxWidth() .height(64.dp) .background( - MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(28.dp) - ), + MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { @@ -224,9 +214,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP .width(selectedRect!!.width.toDp()) .background( MaterialTheme.colorScheme.tertiaryContainer, - shape = RoundedCornerShape(20.dp) - ) - ) + shape = RoundedCornerShape(20.dp))) } } Row { @@ -238,9 +226,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP .padding(horizontal = 8.dp) .height(48.dp) .background( - Color.Transparent, - shape = RoundedCornerShape(28.dp) - ) + Color.Transparent, shape = RoundedCornerShape(28.dp)) .onGloballyPositioned { layoutCoordinates -> if (selected) { selectedRect = layoutCoordinates.boundsInParent() @@ -252,22 +238,16 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP Button( onClick = { multiTogglePreference.updateState( - DeviceSettingStateModel.MultiTogglePreferenceState(idx) - ) + DeviceSettingStateModel.MultiTogglePreferenceState(idx)) }, modifier = Modifier.fillMaxSize(), colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = LocalContentColor.current - ), + contentColor = LocalContentColor.current), ) { - Icon( - bitmap = toggle.icon.asImageBitmap(), - null, - modifier = Modifier.size(24.dp), - tint = LocalContentColor.current - ) + DeviceSettingComposeIcon( + toggle.icon, modifier = Modifier.size(24.dp)) } } } @@ -285,8 +265,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP text = toggle.label, fontSize = 12.sp, textAlign = TextAlign.Center, - modifier = Modifier.weight(1f).padding(horizontal = 8.dp) - ) + modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) } } } diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt new file mode 100644 index 00000000000..87e2e8b4962 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth.ui.layout + +import kotlinx.coroutines.flow.Flow + +/** Represent the layout of device settings. */ +data class DeviceSettingLayout(val rows: List) + +/** Represent a row in the layout. */ +data class DeviceSettingLayoutRow(val settingIds: Flow>) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt new file mode 100644 index 00000000000..b75579dfa0d --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -0,0 +1,226 @@ +/* + * 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.ui.view + +import android.bluetooth.BluetoothAdapter +import android.content.Context +import android.media.AudioManager +import android.util.Log +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import com.android.settings.SettingsPreferenceFragment +import com.android.settings.bluetooth.ui.composable.Icon +import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup +import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout +import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel +import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settings.spa.preference.ComposePreference +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import com.android.settingslib.spa.widget.preference.Preference as SpaPreference + + +/** Handles device details fragment layout according to config. */ +interface DeviceDetailsFragmentFormatter { + /** Gets keys of visible preferences in built-in preference in xml. */ + fun getVisiblePreferenceKeysForMainPage(): List? + + /** Updates device details fragment layout. */ + fun updateLayout() +} + +@OptIn(ExperimentalCoroutinesApi::class) +class DeviceDetailsFragmentFormatterImpl( + private val context: Context, + private val fragment: SettingsPreferenceFragment, + bluetoothAdapter: BluetoothAdapter, + private val cachedDevice: CachedBluetoothDevice +) : DeviceDetailsFragmentFormatter { + private val repository = + featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( + context, bluetoothAdapter, fragment.lifecycleScope) + private val spatialAudioInteractor = + featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( + context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope) + private val viewModel: BluetoothDeviceDetailsViewModel = + ViewModelProvider( + fragment, + BluetoothDeviceDetailsViewModel.Factory( + repository, + spatialAudioInteractor, + cachedDevice, + )) + .get(BluetoothDeviceDetailsViewModel::class.java) + + override fun getVisiblePreferenceKeysForMainPage(): List? = runBlocking { + viewModel + .getItems() + ?.filterIsInstance() + ?.mapNotNull { it.preferenceKey } + } + + /** Updates bluetooth device details fragment layout. */ + override fun updateLayout() = runBlocking { + val items = viewModel.getItems() ?: return@runBlocking + val layout = viewModel.getLayout() ?: return@runBlocking + val prefKeyToSettingId = + items + .filterIsInstance() + .associateBy({ it.preferenceKey }, { it.settingId }) + + val settingIdToXmlPreferences: MutableMap = HashMap() + for (i in 0 until fragment.preferenceScreen.preferenceCount) { + val pref = fragment.preferenceScreen.getPreference(i) + prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref } + } + fragment.preferenceScreen.removeAll() + + for (row in items.indices) { + val settingId = items[row].settingId + if (settingIdToXmlPreferences.containsKey(settingId)) { + fragment.preferenceScreen.addPreference( + settingIdToXmlPreferences[settingId]!!.apply { order = row }) + } else { + val pref = + ComposePreference(context) + .apply { + key = getPreferenceKey(settingId) + order = row + } + .also { pref -> pref.setContent { buildPreference(layout, row) } } + fragment.preferenceScreen.addPreference(pref) + } + } + } + + @Composable + private fun buildPreference(layout: DeviceSettingLayout, row: Int) { + val contents by + remember(row) { + layout.rows[row].settingIds.flatMapLatest { settingIds -> + if (settingIds.isEmpty()) { + flowOf(emptyList()) + } else { + combine( + settingIds.map { settingId -> + viewModel.getDeviceSetting(cachedDevice, settingId) + }) { + it.toList() + } + } + } + } + .collectAsStateWithLifecycle(initialValue = listOf()) + + val settings = contents + when (settings.size) { + 0 -> {} + 1 -> { + when (val setting = settings[0]) { + is DeviceSettingModel.ActionSwitchPreference -> { + buildActionSwitchPreference(setting) + } + is DeviceSettingModel.MultiTogglePreference -> { + buildMultiTogglePreference(listOf(setting)) + } + null -> {} + else -> { + Log.w(TAG, "Unknown preference type ${setting.id}, skip.") + } + } + } + else -> { + if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) { + return + } + buildMultiTogglePreference( + settings.filterIsInstance()) + } + } + } + + @Composable + private fun buildMultiTogglePreference(prefs: List) { + MultiTogglePreferenceGroup(prefs) + } + + @Composable + private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) { + if (model.switchState != null) { + val switchPrefModel = + object : SwitchPreferenceModel { + override val title = model.title + override val summary = { model.summary ?: "" } + override val checked = { model.switchState?.checked } + override val onCheckedChange = { newChecked: Boolean -> + model.updateState?.invoke( + DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked)) + Unit + } + override val icon = @Composable { deviceSettingIcon(model) } + } + if (model.intent != null) { + TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) } + } else { + SwitchPreference(switchPrefModel) + } + } else { + SpaPreference( + object : PreferenceModel { + override val title = model.title + override val summary = { model.summary ?: "" } + override val onClick = { + model.intent?.let { context.startActivity(it) } + Unit + } + override val icon = @Composable { deviceSettingIcon(model) } + }) + } + } + + @Composable + private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) { + model.icon?.let { icon -> + Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize)) + } + } + + private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" + + companion object { + const val TAG = "DeviceDetailsFormatter" + } +} diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt new file mode 100644 index 00000000000..befff830da3 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -0,0 +1,121 @@ +/* + * 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.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor +import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout +import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId +import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class BluetoothDeviceDetailsViewModel( + private val deviceSettingRepository: DeviceSettingRepository, + private val spatialAudioInteractor: SpatialAudioInteractor, + private val cachedDevice: CachedBluetoothDevice, +) : ViewModel() { + private val items = + viewModelScope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { + deviceSettingRepository.getDeviceSettingsConfig(cachedDevice) + } + + suspend fun getItems(): List? = items.await()?.mainItems + + fun getDeviceSetting( + cachedDevice: CachedBluetoothDevice, + @DeviceSettingId settingId: Int + ): Flow { + return when (settingId) { + DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE -> + spatialAudioInteractor.getDeviceSetting(cachedDevice) + else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) + } + } + + suspend fun getLayout(): DeviceSettingLayout? { + val configItems = getItems() ?: return null + val idToDeviceSetting = + configItems + .filterIsInstance() + .associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) }) + + val configDeviceSetting = + configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) } + val positionToSettingIds = + combine(configDeviceSetting) { settings -> + val positionMapping = mutableMapOf>() + var multiToggleSettingIds: MutableList? = null + for (i in settings.indices) { + val configItem = configItems[i] + val setting = settings[i] + val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem + if (!isXmlPreference && setting == null) { + continue + } + if (setting !is DeviceSettingModel.MultiTogglePreference) { + multiToggleSettingIds = null + positionMapping[i] = listOf(configItem.settingId) + continue + } + + if (multiToggleSettingIds != null) { + multiToggleSettingIds.add(setting.id) + } else { + multiToggleSettingIds = mutableListOf(setting.id) + positionMapping[i] = multiToggleSettingIds + } + } + positionMapping + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) + return DeviceSettingLayout( + configItems.indices.map { idx -> + DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() }) + }) + } + + class Factory( + private val deviceSettingRepository: DeviceSettingRepository, + private val spatialAudioInteractor: SpatialAudioInteractor, + private val cachedDevice: CachedBluetoothDevice, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return BluetoothDeviceDetailsViewModel( + deviceSettingRepository, spatialAudioInteractor, cachedDevice) + as T + } + } + + companion object { + private const val TAG = "BluetoothDeviceDetailsViewModel" + } +} diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 666d24dc50c..0df822af185 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -365,12 +365,19 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra if (isChecked != developmentEnabledState) { if (isChecked) { final int userId = getContext().getUserId(); - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getContext(), - mIsBiometricsAuthenticated, - false /* biometricsAuthenticationRequested */, userId)) { + + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics( + getContext(), + mIsBiometricsAuthenticated, + userId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { mSwitchBar.setChecked(false); Utils.launchBiometricPromptForMandatoryBiometrics(this, - REQUEST_BIOMETRIC_PROMPT, userId, false /* hideBackground */); + REQUEST_BIOMETRIC_PROMPT, + userId, false /* hideBackground */); + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + mSwitchBar.setChecked(false); } else { //Reset biometrics once enable dialog is shown mIsBiometricsAuthenticated = false; diff --git a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java index cf6b3e33e76..a9f94b49b45 100644 --- a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java +++ b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java @@ -225,13 +225,15 @@ public class BuildNumberPreferenceController extends BasePreferenceController im if (requestCode == REQUEST_CONFIRM_PASSWORD_FOR_DEV_PREF && resultCode == Activity.RESULT_OK) { final int userId = mContext.getUserId(); - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, - userId)) { + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + false /* biometricsAuthenticationRequested */, + userId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { Utils.launchBiometricPromptForMandatoryBiometrics(mFragment, - REQUEST_IDENTITY_CHECK_FOR_DEV_PREF, userId, false /* hideBackground */); - } else { + REQUEST_IDENTITY_CHECK_FOR_DEV_PREF, + userId, false /* hideBackground */); + } else if (biometricAuthStatus == Utils.BiometricStatus.NOT_ACTIVE) { enableDevelopmentSettings(); } } else if (requestCode == REQUEST_IDENTITY_CHECK_FOR_DEV_PREF diff --git a/src/com/android/settings/localepicker/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java index b284c8d7c40..2294b9b5824 100644 --- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java +++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java @@ -37,6 +37,7 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ListView; +import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.view.ViewCompat; @@ -67,6 +68,7 @@ public class AppLocalePickerActivity extends SettingsBaseActivity private View mAppLocaleDetailContainer; private NotificationController mNotificationController; private MetricsFeatureProvider mMetricsFeatureProvider; + @Nullable private String mParentLocale; @Override public void onCreate(Bundle savedInstanceState) { @@ -129,6 +131,11 @@ public class AppLocalePickerActivity extends SettingsBaseActivity finish(); } + @Override + public void onParentLocaleSelected(LocaleStore.LocaleInfo localeInfo) { + mParentLocale = localeInfo.getFullNameNative(); + } + @Override public boolean onMenuItemActionCollapse(MenuItem item) { mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); @@ -258,6 +265,12 @@ public class AppLocalePickerActivity extends SettingsBaseActivity super.onFragmentViewCreated(fm, f, v, s); ListView listView = (ListView) v.findViewById(android.R.id.list); if (listView != null) { + if (mParentLocale != null) { + mAppLocaleDetails = AppLocaleDetails.newInstance(mPackageName, + getUserId()); + mAppLocaleDetailContainer = launchAppLocaleDetailsPage(); + mAppLocaleDetails.setParentLocale(mParentLocale); + } listView.addHeaderView(mAppLocaleDetailContainer); } } diff --git a/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java index 6e596e16883..05cb6a48d1e 100644 --- a/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java +++ b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java @@ -39,6 +39,7 @@ public class LocalePickerWithRegionActivity extends SettingsBaseActivity implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener { private static final String TAG = LocalePickerWithRegionActivity.class.getSimpleName(); private static final String PARENT_FRAGMENT_NAME = "localeListEditor"; + private static final String CHILD_FRAGMENT_NAME = "LocalePickerWithRegion"; private LocalePickerWithRegion mSelector; @@ -68,12 +69,15 @@ public class LocalePickerWithRegionActivity extends SettingsBaseActivity explicitLocales, null /* appPackageName */, this); - getFragmentManager() - .beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - .replace(R.id.content_frame, mSelector) - .addToBackStack(PARENT_FRAGMENT_NAME) - .commit(); + + if (getFragmentManager().findFragmentByTag(CHILD_FRAGMENT_NAME) == null) { + getFragmentManager() + .beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .replace(R.id.content_frame, mSelector, CHILD_FRAGMENT_NAME) + .addToBackStack(PARENT_FRAGMENT_NAME) + .commit(); + } } @Override diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java index 0fcfcb515a1..e2406826320 100644 --- a/src/com/android/settings/network/NetworkProviderSettings.java +++ b/src/com/android/settings/network/NetworkProviderSettings.java @@ -1068,6 +1068,10 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment @VisibleForTesting void launchNetworkDetailsFragment(LongPressWifiEntryPreference pref) { final WifiEntry wifiEntry = pref.getWifiEntry(); + if (!wifiEntry.isSaved()) { + Log.w(TAG, "launchNetworkDetailsFragment: Don't launch because WifiEntry isn't saved!"); + return; + } final Context context = requireContext(); final Bundle bundle = new Bundle(); diff --git a/src/com/android/settings/network/telephony/NetworkSelectRepository.kt b/src/com/android/settings/network/telephony/NetworkSelectRepository.kt index 1f5fbc202f5..d95c90e5c19 100644 --- a/src/com/android/settings/network/telephony/NetworkSelectRepository.kt +++ b/src/com/android/settings/network/telephony/NetworkSelectRepository.kt @@ -18,8 +18,10 @@ package com.android.settings.network.telephony import android.content.Context import android.telephony.AccessNetworkConstants +import android.telephony.CarrierConfigManager import android.telephony.NetworkRegistrationInfo import android.telephony.TelephonyManager +import android.telephony.satellite.SatelliteManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -28,9 +30,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class NetworkSelectRepository(context: Context, subId: Int) { +class NetworkSelectRepository(context: Context, private val subId: Int) { private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId) + private val satelliteManager = context.getSystemService(SatelliteManager::class.java) + private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java) data class NetworkRegistrationAndForbiddenInfo( val networkList: List, @@ -55,10 +59,21 @@ class NetworkSelectRepository(context: Context, subId: Int) { if (telephonyManager.dataState != TelephonyManager.DATA_CONNECTED) return null // Try to get the network registration states val serviceState = telephonyManager.serviceState ?: return null - val networkList = serviceState.getNetworkRegistrationInfoListForTransportType( + var networkList = serviceState.getNetworkRegistrationInfoListForTransportType( AccessNetworkConstants.TRANSPORT_TYPE_WWAN ) if (networkList.isEmpty()) return null + + val satellitePlmn = getSatellitePlmns() + // If connected network is Satellite, filter out + if (satellitePlmn.isNotEmpty()) { + val filteredNetworkList = networkList.filter { + val cellIdentity = it.cellIdentity + val plmn = cellIdentity?.plmn + plmn != null && !satellitePlmn.contains(plmn) + } + networkList = filteredNetworkList + } // Due to the aggregation of cell between carriers, it's possible to get CellIdentity // containing forbidden PLMN. // Getting current network from ServiceState is no longer a good idea. @@ -72,4 +87,24 @@ class NetworkSelectRepository(context: Context, subId: Int) { private fun getForbiddenPlmns(): List { return telephonyManager.forbiddenPlmns?.toList() ?: emptyList() } + + /** + * Update satellite PLMNs from the satellite framework. + */ + private fun getSatellitePlmns(): List { + val config = carrierConfigManager.getConfigForSubId( + subId, + CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL + ) + + val shouldFilter = config.getBoolean( + CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, + true) + + return if (shouldFilter) { + satelliteManager.getSatellitePlmnsForCarrier(subId) + } else { + emptyList(); + } + } } diff --git a/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java index 200a47b54eb..778c788e4c8 100644 --- a/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java +++ b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java @@ -226,7 +226,10 @@ public class AppChannelsBypassingDndPreferenceController extends NotificationPre .setArguments(channelArgs) .setUserHandle(UserHandle.of(mAppRow.userId)) .setTitleRes(com.android.settings.R.string.notification_channel_title) - .setSourceMetricsCategory(SettingsEnums.DND_APPS_BYPASSING) + .setSourceMetricsCategory( + android.app.Flags.modesUi() + ? SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP_CHANNELS + : SettingsEnums.DND_APPS_BYPASSING) .launch(); return true; }); diff --git a/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java b/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java index 4fab7e277ea..b5e2b130b27 100644 --- a/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java +++ b/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java @@ -40,7 +40,9 @@ public class AppChannelsBypassingDndSettings extends NotificationSettings { @Override public int getMetricsCategory() { - return SettingsEnums.DND_APPS_BYPASSING; + return android.app.Flags.modesUi() + ? SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP_CHANNELS + : SettingsEnums.DND_APPS_BYPASSING; } @Override diff --git a/src/com/android/settings/notification/modes/CircularIconsPreference.java b/src/com/android/settings/notification/modes/CircularIconsPreference.java index 0766ccd5623..ccf7f529e46 100644 --- a/src/com/android/settings/notification/modes/CircularIconsPreference.java +++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java @@ -49,7 +49,9 @@ public class CircularIconsPreference extends RestrictedPreference { private static final float DISABLED_ITEM_ALPHA = 0.3f; - record LoadedIcons(ImmutableList icons, int extraItems) { } + record LoadedIcons(ImmutableList icons, int extraItems) { + static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0); + } private Executor mUiExecutor; @@ -126,6 +128,7 @@ public class CircularIconsPreference extends RestrictedPreference { // We know what icons we want, but haven't yet loaded them. if (mIconSet.size() == 0) { container.setVisibility(View.GONE); + mLoadedIcons = LoadedIcons.EMPTY; return; } container.setVisibility(View.VISIBLE); @@ -137,7 +140,7 @@ public class CircularIconsPreference extends RestrictedPreference { @Override public void onGlobalLayout() { container.getViewTreeObserver().removeOnGlobalLayoutListener(this); - startLoadingIcons(container, mIconSet); + notifyChanged(); } } ); diff --git a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java index f26de76844b..830baaf7bfa 100644 --- a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java +++ b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java @@ -25,6 +25,7 @@ import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; import android.app.ActionBar; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -164,7 +165,8 @@ public class SetupInterstitialActivity extends FragmentActivity { // they happen to go back. Forward the activity result in case we got here (indirectly) // from some app that is waiting for the result. if (updated) { - ZenSubSettingLauncher.forMode(this, modeId) + ZenSubSettingLauncher.forModeFragment(this, ZenModeFragment.class, modeId, + SettingsEnums.ZEN_MODE_INTERSTITIAL) .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT).launch(); } finish(); diff --git a/src/com/android/settings/notification/modes/ZenModeAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java index 19035ddcf78..ec72c83f3a5 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java @@ -47,7 +47,6 @@ public class ZenModeAppsFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; + return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS; } } diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java index 1521a8b4f09..45287abdc61 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java @@ -20,7 +20,10 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; import android.app.Application; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; @@ -48,6 +51,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.function.Function; /** * Preference with a link and summary about what apps can break through the mode @@ -64,24 +68,26 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr private ZenMode mZenMode; private CircularIconsPreference mPreference; private final Fragment mHost; + private final Function mAppIconRetriever; ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host, ZenModesBackend backend, ZenHelperBackend helperBackend) { this(context, key, host, ApplicationsState.getInstance((Application) context.getApplicationContext()), - backend, helperBackend); + backend, helperBackend, appInfo -> Utils.getBadgedIcon(context, appInfo)); } @VisibleForTesting ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host, ApplicationsState applicationsState, ZenModesBackend backend, - ZenHelperBackend helperBackend) { + ZenHelperBackend helperBackend, Function appIconRetriever) { super(context, key, backend); mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); mHelperBackend = helperBackend; mApplicationsState = applicationsState; mUserManager = context.getSystemService(UserManager.class); mHost = host; + mAppIconRetriever = appIconRetriever; } @Override @@ -93,10 +99,9 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr public void updateState(Preference preference, @NonNull ZenMode zenMode) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId()); - // TODO(b/332937635): Update metrics category preference.setIntent( ZenSubSettingLauncher.forModeFragment(mContext, ZenModeAppsFragment.class, - zenMode.getId(), 0).toIntent()); + zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); preference.setEnabled(zenMode.isEnabled()); mZenMode = zenMode; @@ -105,13 +110,18 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) { mPreference.setSummary(R.string.zen_mode_apps_none_apps); mPreference.displayIcons(CircularIconSet.EMPTY); + if (mAppSession != null) { + mAppSession.deactivateSession(); + } } else { if (TextUtils.isEmpty(mPreference.getSummary())) { mPreference.setSummary(R.string.zen_mode_apps_calculating); } - if (mApplicationsState != null && mHost != null) { + if (mAppSession == null) { mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, mHost.getLifecycle()); + } else { + mAppSession.activateSession(); } triggerUpdateAppsBypassingDnd(); } @@ -133,12 +143,16 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr } private void displayAppsBypassingDnd(List allApps) { + if (mZenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) { + // Can get this callback when resuming, if we had CHANNEL_POLICY_PRIORITY and just + // switched to CHANNEL_POLICY_NONE. + return; + } + ImmutableList apps = getAppsBypassingDndSortedByName(allApps); - mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps)); - mPreference.displayIcons(new CircularIconSet<>(apps, - app -> Utils.getBadgedIcon(mContext, app.info)), + app -> mAppIconRetriever.apply(app.info)), APP_ENTRY_EQUIVALENCE); } diff --git a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java index 522f191c37f..c44661a9d09 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java @@ -106,10 +106,9 @@ public class ZenModeAppsPreferenceController extends if (mModeId != null) { bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, mModeId); } - // TODO(b/332937635): Update metrics category new SubSettingLauncher(mContext) .setDestination(ZenModeSelectBypassingAppsFragment.class.getName()) - .setSourceMetricsCategory(SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS) + .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS) .setArguments(bundle) .launch(); } diff --git a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java index 54072ac185a..ac05328d2d3 100644 --- a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java @@ -50,7 +50,6 @@ public class ZenModeCallsFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.DND_CALLS; } diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java index d8850191762..efddcf9e23a 100644 --- a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; @@ -41,10 +42,9 @@ class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceCont public void updateState(Preference preference, @NonNull ZenMode zenMode) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId()); - // TODO(b/332937635): Update metrics category preference.setIntent(new SubSettingLauncher(mContext) .setDestination(ZenModeCallsFragment.class.getName()) - .setSourceMetricsCategory(0) + .setSourceMetricsCategory(SettingsEnums.DND_PEOPLE) .setArguments(bundle) .toIntent()); preference.setSummary(mSummaryHelper.getCallsSettingSummary(zenMode)); diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java index 38ac8f31072..74ed38f96bd 100644 --- a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java @@ -54,7 +54,6 @@ public class ZenModeDisplayFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return SettingsEnums.DND_PEOPLE; + return SettingsEnums.ZEN_MODE_DISPLAY_SETTINGS; } } diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java index bba5e342732..57dce89a35c 100644 --- a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; @@ -41,10 +42,9 @@ class ZenModeDisplayLinkPreferenceController extends AbstractZenModePreferenceCo void updateState(Preference preference, @NonNull ZenMode zenMode) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId()); - // TODO(b/332937635): Update metrics category preference.setIntent( ZenSubSettingLauncher.forModeFragment(mContext, ZenModeDisplayFragment.class, - zenMode.getId(), 0).toIntent()); + zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); preference.setEnabled(zenMode.isEnabled()); } diff --git a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java index a0c2cf1109b..60f731627f8 100644 --- a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java @@ -72,8 +72,7 @@ public class ZenModeEditNameIconFragment extends ZenModeEditNameIconFragmentBase @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; + return SettingsEnums.ZEN_MODE_EDIT_NAME_ICON; } @Override diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 0a80977a021..37772990152 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -129,8 +129,7 @@ public class ZenModeFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; + return SettingsEnums.ZEN_PRIORITY_MODE; } @Override @@ -164,9 +163,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { if (menuItem.getItemId() == RENAME_MODE) { - // TODO: b/332937635 - Update metrics category ZenSubSettingLauncher.forModeFragment(mContext, ZenModeEditNameIconFragment.class, - mZenMode.getId(), 0).launch(); + mZenMode.getId(), getMetricsCategory()).launch(); } else if (menuItem.getItemId() == DELETE_MODE) { new AlertDialog.Builder(mContext) .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation, diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java index 8bf574f4ced..709e5da03df 100644 --- a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java @@ -46,7 +46,6 @@ public class ZenModeMessagesFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.DND_MESSAGES; } diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java index 4c0b758e7cb..50d7958120b 100644 --- a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; @@ -40,10 +41,9 @@ class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceC public void updateState(Preference preference, @NonNull ZenMode zenMode) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId()); - // TODO(b/332937635): Update metrics category preference.setIntent(new SubSettingLauncher(mContext) .setDestination(ZenModeMessagesFragment.class.getName()) - .setSourceMetricsCategory(0) + .setSourceMetricsCategory(SettingsEnums.DND_PEOPLE) .setArguments(bundle) .toIntent()); diff --git a/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java b/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java index 6086c0c7b38..d7dbaaf8702 100644 --- a/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java @@ -16,6 +16,8 @@ package com.android.settings.notification.modes; +import android.app.settings.SettingsEnums; + import androidx.annotation.Nullable; import com.android.settings.R; @@ -50,15 +52,15 @@ public class ZenModeNewCustomFragment extends ZenModeEditNameIconFragmentBase { if (created != null) { // Open the mode view fragment and close the "add mode" fragment, so exiting the mode // view goes back to previous screen (which should be the modes list). - ZenSubSettingLauncher.forMode(requireContext(), created.getId()).launch(); + ZenSubSettingLauncher.forModeFragment(requireContext(), ZenModeFragment.class, + created.getId(), getMetricsCategory()).launch(); finish(); } } @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return 0; + return SettingsEnums.ZEN_MODE_ADD_NEW; } @Override diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java index 3fdfec6e106..d1bd493482a 100644 --- a/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java @@ -19,6 +19,7 @@ package com.android.settings.notification.modes; import android.app.settings.SettingsEnums; import android.content.Context; import android.service.notification.ZenPolicy; + import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -57,7 +58,6 @@ public class ZenModeNotifVisFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category - return SettingsEnums.DND_PEOPLE; + return SettingsEnums.ZEN_CUSTOM_RULE_VIS_EFFECTS; } } diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java index 622c4a2db48..cd1e8c7d3ba 100644 --- a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.notification.modes; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; @@ -47,10 +48,9 @@ class ZenModeNotifVisLinkPreferenceController extends AbstractZenModePreferenceC public void updateState(Preference preference, @NonNull ZenMode zenMode) { Bundle bundle = new Bundle(); bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId()); - // TODO(b/332937635): Update metrics category preference.setIntent(new SubSettingLauncher(mContext) .setDestination(ZenModeNotifVisFragment.class.getName()) - .setSourceMetricsCategory(0) + .setSourceMetricsCategory(SettingsEnums.ZEN_MODE_DISPLAY_SETTINGS) .setArguments(bundle) .toIntent()); } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java index 1149cd1312f..28b2e54d3a6 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java @@ -16,14 +16,9 @@ package com.android.settings.notification.modes; -import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; -import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; -import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; -import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; -import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; - import android.app.settings.SettingsEnums; import android.content.Context; + import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -58,7 +53,6 @@ public class ZenModeOtherFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; } } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index 15e0edcf1df..5b26364c938 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -23,6 +23,7 @@ import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; +import android.app.settings.SettingsEnums; import android.content.Context; import android.service.notification.ZenPolicy; @@ -65,10 +66,9 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont @Override public void updateState(Preference preference, @NonNull ZenMode zenMode) { - // TODO: b/332937635 - Update metrics category preference.setIntent( ZenSubSettingLauncher.forModeFragment(mContext, ZenModeOtherFragment.class, - zenMode.getId(), 0).toIntent()); + zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)); diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java index f541d132010..11e4453ddef 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java @@ -48,7 +48,6 @@ public class ZenModePeopleFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.DND_PEOPLE; } } diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index 4610c35ca82..9aad4603588 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -26,6 +26,7 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; import static android.service.notification.ZenPolicy.STATE_ALLOW; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.LauncherApps; import android.graphics.drawable.Drawable; @@ -88,10 +89,10 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon @Override public void updateState(Preference preference, @NonNull ZenMode zenMode) { - // TODO(b/332937635): Update metrics category + // Passes in source ZenModeFragment metric category. preference.setIntent( ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class, - zenMode.getId(), 0).toIntent()); + zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy())); diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java index ab5e2d9e56b..11b65bd21f5 100644 --- a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java @@ -270,10 +270,9 @@ class ZenModePrioritySendersPreferenceController mContext.startActivity(ALL_CONTACTS_INTENT); } else if (KEY_ANY_CONVERSATIONS.equals(key) || KEY_IMPORTANT_CONVERSATIONS.equals(key)) { - // TODO: b/332937635 - set correct metrics category new SubSettingLauncher(mContext) .setDestination(ConversationListSettings.class.getName()) - .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) + .setSourceMetricsCategory(SettingsEnums.DND_MESSAGES) .launch(); } else { mContext.startActivity(FALLBACK_INTENT); diff --git a/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java index 6202648fcf9..d129aad6ad6 100644 --- a/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java +++ b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.net.Uri; import android.os.Bundle; @@ -70,8 +71,7 @@ public class ZenModeScheduleChooserDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - // TODO: b/332937635 - Update metrics category - return 0; + return SettingsEnums.ZEN_SCHEDULE_CHOOSER_DIALOG; } static void show(DashboardFragment parent, OnScheduleOptionListener optionListener) { diff --git a/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java index 8b682b92f1a..1f5438d08eb 100644 --- a/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java @@ -74,8 +74,7 @@ public class ZenModeSelectBypassingAppsFragment extends ZenModeFragmentBase impl @Override public int getMetricsCategory() { - // TODO(b/332937635): Update metrics category - return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS; + return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP; } /** diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java index f0206ef5dad..a266c8b1637 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java @@ -46,7 +46,6 @@ public class ZenModeSetCalendarFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.NOTIFICATION_ZEN_MODE_EVENT_RULE; } } diff --git a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java index 4d58097b1dc..91197845874 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java @@ -48,7 +48,6 @@ public class ZenModeSetScheduleFragment extends ZenModeFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - make this the correct metrics category return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE; } } diff --git a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java index d8e1b38875b..3fa53946477 100644 --- a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java @@ -62,7 +62,6 @@ public class ZenModeTimePickerFragment extends InstrumentedDialogFragment implem @Override public int getMetricsCategory() { - // TODO: b/332937635 - set correct metrics category (or decide to keep this one?) return SettingsEnums.DIALOG_ZEN_TIMEPICKER; } diff --git a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java index 885c4db1fa1..1add4889c66 100644 --- a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java @@ -24,6 +24,7 @@ import static android.service.notification.ZenModeConfig.tryParseScheduleConditi import android.annotation.SuppressLint; import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -97,9 +98,9 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference private void setUpForSystemOwnedTrigger(Preference preference, ZenMode mode) { if (mode.getType() == TYPE_SCHEDULE_TIME) { - // TODO: b/332937635 - set correct metrics category preference.setIntent(ZenSubSettingLauncher.forModeFragment(mContext, - ZenModeSetScheduleFragment.class, mode.getId(), 0).toIntent()); + ZenModeSetScheduleFragment.class, mode.getId(), + SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); // [Clock Icon] 9:00 - 17:00 / Sun-Mon preference.setIcon(com.android.internal.R.drawable.ic_zen_mode_type_schedule_time); @@ -115,9 +116,9 @@ class ZenModeTriggerUpdatePreferenceController extends AbstractZenModePreference preference.setSummary(null); } } else if (mode.getType() == TYPE_SCHEDULE_CALENDAR) { - // TODO: b/332937635 - set correct metrics category preference.setIntent(ZenSubSettingLauncher.forModeFragment(mContext, - ZenModeSetCalendarFragment.class, mode.getId(), 0).toIntent()); + ZenModeSetCalendarFragment.class, mode.getId(), + SettingsEnums.ZEN_PRIORITY_MODE).toIntent()); // [Event Icon] Calendar Events / preference.setIcon( diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java index 57d3bf96c2b..e7905a8f936 100644 --- a/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java +++ b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -56,8 +57,7 @@ public class ZenModesListAddModeTypeChooserDialog extends InstrumentedDialogFrag @Override public int getMetricsCategory() { - // TODO: b/332937635 - Update metrics category - return 0; + return SettingsEnums.ZEN_MODE_NEW_TYPE_CHOOSER_DIALOG; } static void show(DashboardFragment parent, diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java index cab0209e06c..2b58f8e1c3b 100644 --- a/src/com/android/settings/notification/modes/ZenModesListFragment.java +++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java @@ -74,8 +74,7 @@ public class ZenModesListFragment extends ZenModesFragmentBase { @Override public int getMetricsCategory() { - // TODO: b/332937635 - add new & set metrics categories correctly - return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; + return SettingsEnums.ZEN_PRIORITY_MODES_LIST; } private void onAvailableModeTypesForAdd(List types) { @@ -97,10 +96,9 @@ public class ZenModesListFragment extends ZenModesFragmentBase { startActivityForResult(type.creationActivityIntent(), REQUEST_NEW_MODE); } else { // Custom-manual mode -> "add a mode" screen. - // TODO: b/332937635 - set metrics categories correctly new SubSettingLauncher(requireContext()) .setDestination(ZenModeNewCustomFragment.class.getName()) - .setSourceMetricsCategory(0) + .setSourceMetricsCategory(SettingsEnums.ZEN_PRIORITY_MODES_LIST) .launch(); } } @@ -125,7 +123,9 @@ public class ZenModesListFragment extends ZenModesFragmentBase { .filter(m -> m.getRule().getPackageName().equals(activityInvoked.getPackageName())) .findFirst(); createdZenMode.ifPresent( - mode -> ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch()); + mode -> + ZenSubSettingLauncher.forModeFragment(mContext, ZenModeFragment.class, + mode.getId(), getMetricsCategory()).launch()); } /** diff --git a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java index e09d04c7737..0c961481193 100644 --- a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java +++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java @@ -15,6 +15,7 @@ */ package com.android.settings.notification.modes; +import android.app.settings.SettingsEnums; import android.content.Context; import android.widget.TextView; @@ -63,7 +64,8 @@ class ZenModesListItemPreference extends RestrictedPreference { @Override public void onClick() { - ZenSubSettingLauncher.forMode(mContext, mZenMode.getId()).launch(); + ZenSubSettingLauncher.forModeFragment(mContext, ZenModeFragment.class, mZenMode.getId(), + SettingsEnums.ZEN_PRIORITY_MODES_LIST).launch(); } public void setZenMode(ZenMode zenMode) { diff --git a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java index 00c21bbe4cd..c02a9d95f20 100644 --- a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java +++ b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java @@ -18,7 +18,6 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; -import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; @@ -26,12 +25,6 @@ import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; class ZenSubSettingLauncher { - - static SubSettingLauncher forMode(Context context, String modeId) { - return forModeFragment(context, ZenModeFragment.class, modeId, - SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION); - } - static SubSettingLauncher forModeFragment(Context context, Class fragmentClass, String modeId, int sourceMetricsCategory) { diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index 34c0731184e..09091102f24 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -491,11 +491,16 @@ public class ChooseLockGeneric extends SettingsActivity { ? data.getParcelableExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD) : null; updatePreferencesOrFinish(false /* isRecreatingActivity */); - if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getContext(), - mBiometricsAuthSuccessful, mWaitingForConfirmation, mUserId)) { - mWaitingForConfirmation = true; - Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST, + final Utils.BiometricStatus biometricAuthStatus = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + false /* biometricsAuthenticationRequested */, + mUserId); + if (biometricAuthStatus == Utils.BiometricStatus.OK) { + Utils.launchBiometricPromptForMandatoryBiometrics(this, + BIOMETRIC_AUTH_REQUEST, mUserId, true /* hideBackground */); + } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { + finish(); } } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { if (resultCode == Activity.RESULT_OK) { diff --git a/src/com/android/settings/slices/SlicePreferenceController.java b/src/com/android/settings/slices/SlicePreferenceController.java index 5e8fb26eeb2..2e835a03cde 100644 --- a/src/com/android/settings/slices/SlicePreferenceController.java +++ b/src/com/android/settings/slices/SlicePreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; @@ -61,7 +62,8 @@ public class SlicePreferenceController extends BasePreferenceController implemen return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } - public void setSliceUri(Uri uri) { + /** Sets Slice uri for the preference. */ + public void setSliceUri(@Nullable Uri uri) { mUri = uri; mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> { Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type); diff --git a/src/com/android/settings/users/UserCapabilities.java b/src/com/android/settings/users/UserCapabilities.java index 590cb0cf11a..60e92a8c2ce 100644 --- a/src/com/android/settings/users/UserCapabilities.java +++ b/src/com/android/settings/users/UserCapabilities.java @@ -76,6 +76,9 @@ public class UserCapabilities { public void updateAddUserCapabilities(Context context) { final UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + final UserInfo myUserInfo = userManager.getUserInfo(UserHandle.myUserId()); + mIsAdmin = myUserInfo.isAdmin(); + mEnforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, UserManager.DISALLOW_ADD_USER, UserHandle.myUserId()); final boolean hasBaseUserRestriction = RestrictedLockUtilsInternal.hasBaseUserRestriction( diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index 66c278ed733..8afab9678f4 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -570,7 +570,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment *
  • OR multiple admin support is NOT enabled.
  • *
  • OR the current user has DISALLOW_GRANT_ADMIN restriction applied
  • * - *
  • OR the target user ('mUserInfo') is a main user OR a guest user.
  • + *
  • OR the target user ('mUserInfo') is a main user
  • + *
  • OR the target user ('mUserInfo') is not of type + * {@link UserManager#USER_TYPE_FULL_SECONDARY}
  • *
  • OR the target user ('mUserInfo') has DISALLOW_GRANT_ADMIN restriction.
  • * * @@ -582,7 +584,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment || mUserManager.hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN); boolean targetUserRestricted = mUserInfo.isMain() - || mUserInfo.isGuest() + || !(UserManager.USER_TYPE_FULL_SECONDARY.equals(mUserInfo.userType)) || mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_GRANT_ADMIN, mUserInfo.getUserHandle()); diff --git a/tests/robotests/src/com/android/settings/MainClearTest.java b/tests/robotests/src/com/android/settings/MainClearTest.java index 26a430b161c..b705ae14cac 100644 --- a/tests/robotests/src/com/android/settings/MainClearTest.java +++ b/tests/robotests/src/com/android/settings/MainClearTest.java @@ -142,7 +142,7 @@ public class MainClearTest { when(mMockActivity.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager); when(mBiometricManager.canAuthenticate(anyInt(), eq(BiometricManager.Authenticators.MANDATORY_BIOMETRICS))) - .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); + .thenReturn(BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE); } @After @@ -388,6 +388,30 @@ public class MainClearTest { verify(mMainClear, times(0)).showFinalConfirmation(); } + @Test + @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testOnActivityResultInternal_keyguardRequestNotTriggeringBiometricPrompt_lockoutError() { + when(mContext.getResources()).thenReturn(mResources); + when(mMockActivity.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager); + when(mResources.getString(anyInt())).thenReturn(TEST_ACCOUNT_NAME); + when(mBiometricManager.canAuthenticate(anyInt(), + eq(BiometricManager.Authenticators.MANDATORY_BIOMETRICS))) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_LOCKOUT); + doReturn(true).when(mMainClear).isValidRequestCode(eq(MainClear.KEYGUARD_REQUEST)); + doNothing().when(mMainClear).startActivityForResult(any(), anyInt()); + doReturn(mMockActivity).when(mMainClear).getActivity(); + doReturn(mContext).when(mMainClear).getContext(); + + mMainClear + .onActivityResultInternal(MainClear.KEYGUARD_REQUEST, Activity.RESULT_OK, null); + + verify(mMainClear).isValidRequestCode(eq(MainClear.KEYGUARD_REQUEST)); + verify(mMainClear, never()).startActivityForResult(any(), eq(MainClear.BIOMETRICS_REQUEST)); + verify(mMainClear, never()).establishInitialState(); + verify(mMainClear, never()).getAccountConfirmationIntent(); + verify(mMainClear, never()).showFinalConfirmation(); + } + @Test public void testOnActivityResultInternal_biometricRequestTriggeringFinalConfirmation() { doReturn(true).when(mMainClear).isValidRequestCode(eq(MainClear.BIOMETRICS_REQUEST)); @@ -397,10 +421,10 @@ public class MainClearTest { mMainClear .onActivityResultInternal(MainClear.BIOMETRICS_REQUEST, Activity.RESULT_OK, null); - verify(mMainClear, times(1)).isValidRequestCode(eq(MainClear.BIOMETRICS_REQUEST)); - verify(mMainClear, times(0)).establishInitialState(); - verify(mMainClear, times(1)).getAccountConfirmationIntent(); - verify(mMainClear, times(1)).showFinalConfirmation(); + verify(mMainClear).isValidRequestCode(eq(MainClear.BIOMETRICS_REQUEST)); + verify(mMainClear, never()).establishInitialState(); + verify(mMainClear).getAccountConfirmationIntent(); + verify(mMainClear).showFinalConfirmation(); } @Test diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java index 2aeb9063b1e..107a1b333df 100644 --- a/tests/robotests/src/com/android/settings/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/UtilsTest.java @@ -530,40 +530,40 @@ public class UtilsTest { @Test @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) - public void testRequestBiometricAuthentication_biometricManagerNull_shouldReturnFalse() { + public void testRequestBiometricAuthentication_biometricManagerNull_shouldReturnNotActive() { when(mContext.getSystemService(BiometricManager.class)).thenReturn(null); assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, USER_ID)).isFalse(); + false /* biometricsAuthenticationRequested */, USER_ID)).isEqualTo( + Utils.BiometricStatus.NOT_ACTIVE); } @Test @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) - public void testRequestBiometricAuthentication_biometricManagerReturnsSuccess_shouldReturnTrue() { + public void testRequestBiometricAuthentication_biometricManagerReturnsSuccess_shouldReturnOk() { when(mBiometricManager.canAuthenticate(USER_ID, BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); - final boolean requestBiometricAuthenticationForMandatoryBiometrics = + final Utils.BiometricStatus requestBiometricAuthenticationForMandatoryBiometrics = Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, USER_ID); - assertThat(requestBiometricAuthenticationForMandatoryBiometrics).isTrue(); + false /* biometricsAuthenticationRequested */, USER_ID); + assertThat(requestBiometricAuthenticationForMandatoryBiometrics).isEqualTo( + Utils.BiometricStatus.OK); } @Test @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) - public void testRequestBiometricAuthentication_biometricManagerReturnsError_shouldReturnFalse() { + public void testRequestBiometricAuthentication_biometricManagerReturnsError_shouldReturnError() { when(mBiometricManager.canAuthenticate(anyInt(), eq(BiometricManager.Authenticators.MANDATORY_BIOMETRICS))) .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, USER_ID)).isFalse(); + false /* biometricsAuthenticationRequested */, USER_ID)).isEqualTo( + Utils.BiometricStatus.ERROR); } @Test @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) - public void testRequestBiometricAuthentication_biometricManagerReturnsSuccessForDifferentUser_shouldReturnFalse() { + public void testRequestBiometricAuthentication_biometricManagerReturnsSuccessForDifferentUser_shouldReturnError() { when(mBiometricManager.canAuthenticate(anyInt(), eq(BiometricManager.Authenticators.MANDATORY_BIOMETRICS))) .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); @@ -571,8 +571,8 @@ public class UtilsTest { BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, - false /* biometricsSuccessfullyAuthenticated */, - false /* biometricsAuthenticationRequested */, USER_ID)).isFalse(); + false /* biometricsAuthenticationRequested */, USER_ID)).isEqualTo( + Utils.BiometricStatus.ERROR); } @Test diff --git a/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java index 4f8860e8832..b4605c74785 100644 --- a/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/combination/CombinedBiometricProfileSettingsTest.java @@ -129,7 +129,7 @@ public class CombinedBiometricProfileSettingsTest { doReturn(mBiometricManager).when(mActivity).getSystemService(BiometricManager.class); when(mBiometricManager.canAuthenticate(anyInt(), eq(BiometricManager.Authenticators.MANDATORY_BIOMETRICS))) - .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); + .thenReturn(BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE); ReflectionHelpers.setField(mFragment, "mDashboardFeatureProvider", FakeFeatureFactory.setupForTest().dashboardFeatureProvider); @@ -187,6 +187,8 @@ public class CombinedBiometricProfileSettingsTest { mFragment.onAttach(mContext); mFragment.onCreate(null); + mFragment.onActivityResult(CONFIRM_REQUEST, RESULT_FINISHED, + new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L)); verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), eq(BiometricsSettingsBase.BIOMETRIC_AUTH_REQUEST)); diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java index ca76c1e6069..0e1bcf6348c 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java @@ -20,6 +20,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWE import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; +import static com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST; +import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.CHOOSE_LOCK_GENERIC_REQUEST; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment.KEY_REQUIRE_SCREEN_ON_TO_AUTH; @@ -146,7 +148,7 @@ public class FingerprintSettingsFragmentTest { doReturn(mBiometricManager).when(mContext).getSystemService(BiometricManager.class); doReturn(true).when(mFingerprintManager).isHardwareDetected(); doReturn(mVibrator).when(mContext).getSystemService(Vibrator.class); - when(mBiometricManager.canAuthenticate( + when(mBiometricManager.canAuthenticate(PRIMARY_USER_ID, BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); } @@ -170,20 +172,23 @@ public class FingerprintSettingsFragmentTest { } @Test + @Ignore("b/353706169") @EnableFlags(Flags.FLAG_MANDATORY_BIOMETRICS) public void testLaunchBiometricPromptForFingerprint() { - when(mBiometricManager.canAuthenticate( + when(mBiometricManager.canAuthenticate(PRIMARY_USER_ID, BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); - + doNothing().when(mFingerprintManager).generateChallenge(anyInt(), any()); + when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(true); setUpFragment(false); - ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass( - Intent.class); + ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + mFragment.onActivityResult(CONFIRM_REQUEST, RESULT_FINISHED, + new Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 1L)); verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), eq(BIOMETRIC_AUTH_REQUEST)); - Intent intent = intentArgumentCaptor.getValue(); + final Intent intent = intentArgumentCaptor.getValue(); assertThat(intent.getComponent().getClassName()).isEqualTo( ConfirmDeviceCredentialActivity.InternalActivity.class.getName()); } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java index 50aa7719ccb..19d0eddd3a4 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java @@ -50,6 +50,7 @@ import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -101,6 +102,8 @@ public class BluetoothDeviceDetailsFragmentTest { private InputManager mInputManager; @Mock private CompanionDeviceManager mCompanionDeviceManager; + @Mock + private DeviceDetailsFragmentFormatter mFormatter; @Before public void setUp() { @@ -111,7 +114,10 @@ public class BluetoothDeviceDetailsFragmentTest { .getSystemService(CompanionDeviceManager.class); when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of()); removeInputDeviceWithMatchingBluetoothAddress(); - FakeFeatureFactory.setupForTest(); + FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest(); + when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(), + any(), any(), eq(mCachedDevice))).thenReturn(mFormatter); + when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null); mFragment = setupFragment(); mFragment.onAttach(mContext); diff --git a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt new file mode 100644 index 00000000000..a83b7c2780e --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt @@ -0,0 +1,254 @@ +/* + * 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.domain.interactor + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import android.media.AudioManager +import androidx.test.core.app.ApplicationProvider +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.media.data.repository.SpatializerRepository +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class SpatialAudioInteractorTest { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var cachedDevice: CachedBluetoothDevice + @Mock private lateinit var bluetoothDevice: BluetoothDevice + @Mock private lateinit var spatializerRepository: SpatializerRepository + @Mock private lateinit var leAudioProfile: LeAudioProfile + + private lateinit var underTest: SpatialAudioInteractor + private val testScope = TestScope() + + @Before + fun setUp() { + val context = spy(ApplicationProvider.getApplicationContext()) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS) + `when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO) + underTest = + SpatialAudioInteractorImpl( + context, + audioManager, + SpatializerInteractor(spatializerRepository), + testScope.backgroundScope, + testScope.testScheduler) + } + + @Test + fun getDeviceSetting_noAudioProfile_returnNull() { + testScope.runTest { + val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) + + assertThat(setting).isNull() + verifyNoInteractions(spatializerRepository) + } + } + + @Test + fun getDeviceSetting_audioProfileNotEnabled_returnNull() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false) + + val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) + + assertThat(setting).isNull() + verifyNoInteractions(spatializerRepository) + } + } + + @Test + fun getDeviceSetting_spatialAudioNotSupported_returnNull() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) + `when`( + spatializerRepository.isSpatialAudioAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(false) + + val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) + + assertThat(setting).isNull() + } + } + + @Test + fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) + `when`( + spatializerRepository.isSpatialAudioAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`( + spatializerRepository.isHeadTrackingAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(false) + `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) + .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) + `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(false) + + val setting = + getLatestValue(underTest.getDeviceSetting(cachedDevice)) + as DeviceSettingModel.MultiTogglePreference + + assertThat(setting).isNotNull() + assertThat(setting.toggles.size).isEqualTo(2) + assertThat(setting.state.selectedIndex).isEqualTo(1) + } + } + + @Test + fun getDeviceSetting_headTrackingSupported_returnThreeToggles() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) + `when`( + spatializerRepository.isSpatialAudioAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`( + spatializerRepository.isHeadTrackingAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) + .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) + `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + + val setting = + getLatestValue(underTest.getDeviceSetting(cachedDevice)) + as DeviceSettingModel.MultiTogglePreference + + assertThat(setting).isNotNull() + assertThat(setting.toggles.size).isEqualTo(3) + assertThat(setting.state.selectedIndex).isEqualTo(2) + } + } + + @Test + fun getDeviceSetting_updateState_enableSpatialAudio() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) + `when`( + spatializerRepository.isSpatialAudioAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`( + spatializerRepository.isHeadTrackingAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) + `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(false) + + val setting = + getLatestValue(underTest.getDeviceSetting(cachedDevice)) + as DeviceSettingModel.MultiTogglePreference + setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) + runCurrent() + + assertThat(setting).isNotNull() + verify(spatializerRepository, times(1)) + .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) + } + } + + @Test + fun getDeviceSetting_updateState_enableHeadTracking() { + testScope.runTest { + `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) + `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) + `when`( + spatializerRepository.isSpatialAudioAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`( + spatializerRepository.isHeadTrackingAvailableForDevice( + BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(true) + `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) + `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) + .thenReturn(false) + + val setting = + getLatestValue(underTest.getDeviceSetting(cachedDevice)) + as DeviceSettingModel.MultiTogglePreference + setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) + runCurrent() + + assertThat(setting).isNotNull() + verify(spatializerRepository, times(1)) + .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) + verify(spatializerRepository, times(1)) + .setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true) + } + } + + private fun getLatestValue(deviceSettingFlow: Flow): DeviceSettingModel? { + var latestValue: DeviceSettingModel? = null + deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope) + testScope.runCurrent() + return latestValue + } + + private companion object { + const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34" + val BLE_AUDIO_DEVICE_ATTRIBUTES = + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + BLUETOOTH_ADDRESS, + ) + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt new file mode 100644 index 00000000000..609d7679f16 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -0,0 +1,253 @@ +/* + * 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.ui.view + +import android.bluetooth.BluetoothAdapter +import android.content.Context +import android.graphics.Bitmap +import android.media.AudioManager +import androidx.fragment.app.FragmentActivity +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import androidx.test.core.app.ApplicationProvider +import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId +import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowLooper.shadowMainLooper + +@RunWith(RobolectricTestRunner::class) +class DeviceDetailsFragmentFormatterTest { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var cachedDevice: CachedBluetoothDevice + @Mock private lateinit var bluetoothAdapter: BluetoothAdapter + @Mock private lateinit var repository: DeviceSettingRepository + @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor + + private lateinit var fragment: TestFragment + private lateinit var underTest: DeviceDetailsFragmentFormatter + private lateinit var featureFactory: FakeFeatureFactory + private val testScope = TestScope() + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + featureFactory = FakeFeatureFactory.setupForTest() + `when`( + featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( + eq(context), eq(bluetoothAdapter), any())) + .thenReturn(repository) + `when`( + featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( + eq(context), any(AudioManager::class.java), any())) + .thenReturn(spatialAudioInteractor) + val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java) + assertThat(fragmentActivity.applicationContext).isNotNull() + fragment = TestFragment(context) + fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit() + shadowMainLooper().idle() + + fragment.preferenceScreen.run { + addPreference(Preference(context).apply { key = "bluetooth_device_header" }) + addPreference(Preference(context).apply { key = "action_buttons" }) + addPreference(Preference(context).apply { key = "keyboard_settings" }) + } + + underTest = + DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice) + } + + @Test + fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_HEADER, + "bluetooth_device_header"), + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"), + ), + listOf(), + "footer")) + + val keys = underTest.getVisiblePreferenceKeysForMainPage() + + assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons") + } + } + + @Test + fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null) + + val keys = underTest.getVisiblePreferenceKeysForMainPage() + + assertThat(keys).isNull() + } + } + + @Test + fun updateLayout_configIsNull_notChange() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null) + + underTest.updateLayout() + + assertThat(getDisplayedPreferences().map { it.key }) + .containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings") + } + } + + @Test + fun updateLayout_itemsNotInConfig_hide() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_HEADER, + "bluetooth_device_header"), + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, + "keyboard_settings"), + ), + listOf(), + "footer")) + + underTest.updateLayout() + + assertThat(getDisplayedPreferences().map { it.key }) + .containsExactly("bluetooth_device_header", "keyboard_settings") + } + } + + @Test + fun updateLayout_newItems_displayNewItems() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_HEADER, + "bluetooth_device_header"), + DeviceSettingConfigItemModel.AppProvidedItem( + DeviceSettingId.DEVICE_SETTING_ID_ANC), + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, + "keyboard_settings"), + ), + listOf(), + "footer")) + `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC)) + .thenReturn( + flowOf( + DeviceSettingModel.MultiTogglePreference( + cachedDevice, + DeviceSettingId.DEVICE_SETTING_ID_ANC, + "title", + toggles = + listOf( + ToggleModel( + "", DeviceSettingIcon.BitmapIcon( + Bitmap.createBitmap( + 1, + 1, + Bitmap.Config.ARGB_8888 + ) + ) + ) + ), + isActive = true, + state = DeviceSettingStateModel.MultiTogglePreferenceState(0), + isAllowedChangingState = true, + updateState = {}))) + + underTest.updateLayout() + + assertThat(getDisplayedPreferences().map { it.key }) + .containsExactly( + "bluetooth_device_header", + "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", + "keyboard_settings") + } + } + + private fun getDisplayedPreferences(): List { + val prefs = mutableListOf() + for (i in 0..() + featureFactory = FakeFeatureFactory.setupForTest() + `when`( + featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( + eq(context), eq(bluetoothAdapter), any())) + .thenReturn(repository) + + underTest = + BluetoothDeviceDetailsViewModel(repository, spatialAudioInteractor, cachedDevice) + } + + @Test + fun getItems_returnConfigMainItems() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer")) + + val keys = underTest.getItems() + + assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2) + } + } + + @Test + fun getDeviceSetting_returnRepositoryResponse() { + testScope.runTest { + val remoteSettingId1 = 10001 + val pref = buildMultiTogglePreference(remoteSettingId1) + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + BUILTIN_SETTING_ITEM_1, + buildRemoteSettingItem(remoteSettingId1), + ), + listOf(), + "footer")) + `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1)) + .thenReturn(flowOf(pref)) + + var deviceSetting: DeviceSettingModel? = null + underTest + .getDeviceSetting(cachedDevice, remoteSettingId1) + .onEach { deviceSetting = it } + .launchIn(testScope.backgroundScope) + runCurrent() + + assertThat(deviceSetting).isSameInstanceAs(pref) + verify(repository, times(1)).getDeviceSetting(cachedDevice, remoteSettingId1) + } + } + + @Test + fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() { + testScope.runTest { + val pref = + buildMultiTogglePreference( + DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + BUILTIN_SETTING_ITEM_1, + buildRemoteSettingItem( + DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE), + ), + listOf(), + "footer")) + `when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref)) + + var deviceSetting: DeviceSettingModel? = null + underTest + .getDeviceSetting( + cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) + .onEach { deviceSetting = it } + .launchIn(testScope.backgroundScope) + runCurrent() + + assertThat(deviceSetting).isSameInstanceAs(pref) + verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice) + } + } + + @Test + fun getLayout_builtinDeviceSettings() { + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer")) + + val layout = underTest.getLayout()!! + + assertThat(getLatestLayout(layout)) + .isEqualTo( + listOf( + listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), + listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS))) + } + } + + @Test + fun getLayout_remoteDeviceSettings() { + val remoteSettingId1 = 10001 + val remoteSettingId2 = 10002 + val remoteSettingId3 = 10003 + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + BUILTIN_SETTING_ITEM_1, + buildRemoteSettingItem(remoteSettingId1), + buildRemoteSettingItem(remoteSettingId2), + buildRemoteSettingItem(remoteSettingId3), + ), + listOf(), + "footer")) + `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1)) + .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1))) + `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2)) + .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2))) + `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3)) + .thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3))) + + val layout = underTest.getLayout()!! + + assertThat(getLatestLayout(layout)) + .isEqualTo( + listOf( + listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), + listOf(remoteSettingId1, remoteSettingId2), + listOf(remoteSettingId3), + )) + } + } + + private fun getLatestLayout(layout: DeviceSettingLayout): List> { + var latestLayout = MutableList(layout.rows.size) { emptyList() } + for (i in layout.rows.indices) { + layout.rows[i] + .settingIds + .onEach { latestLayout[i] = it } + .launchIn(testScope.backgroundScope) + } + + testScope.runCurrent() + return latestLayout.filter { !it.isEmpty() }.toList() + } + + private fun buildMultiTogglePreference(settingId: Int) = + DeviceSettingModel.MultiTogglePreference( + cachedDevice, + settingId, + "title", + toggles = + listOf( + ToggleModel( + "toggle1", + DeviceSettingIcon.BitmapIcon( + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))), + isActive = true, + state = DeviceSettingStateModel.MultiTogglePreferenceState(0), + isAllowedChangingState = true, + updateState = {}) + + private fun buildActionSwitchPreference(settingId: Int) = + DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title") + + private fun buildRemoteSettingItem(settingId: Int) = + DeviceSettingConfigItemModel.AppProvidedItem(settingId) + + private companion object { + val BUILTIN_SETTING_ITEM_1 = + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header") + val BUILDIN_SETTING_ITEM_2 = + DeviceSettingConfigItemModel.BuiltinItem( + DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons") + } +} diff --git a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java index 01611788e49..59021a78cda 100644 --- a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java +++ b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java @@ -872,6 +872,17 @@ public class NetworkProviderSettingsTest { verify(mWifiEntry, never()).getKey(); } + @Test + public void launchNetworkDetailsFragment_wifiEntryIsNotSaved_ignoreWifiEntry() { + when(mWifiEntry.isSaved()).thenReturn(false); + LongPressWifiEntryPreference preference = + mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry); + + mNetworkProviderSettings.launchNetworkDetailsFragment(preference); + + verify(mWifiEntry, never()).getKey(); + } + @Implements(PreferenceFragmentCompat.class) public static class ShadowPreferenceFragmentCompat { diff --git a/tests/robotests/src/com/android/settings/notification/modes/IconOptionsProviderImplTest.java b/tests/robotests/src/com/android/settings/notification/modes/IconOptionsProviderImplTest.java index a9bbb4792ef..f0109d69223 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/IconOptionsProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/IconOptionsProviderImplTest.java @@ -34,7 +34,7 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class IconOptionsProviderImplTest { - private static final int EXPECTED_NUMBER_OF_ICON_OPTIONS = 20; + private static final int EXPECTED_NUMBER_OF_ICON_OPTIONS = 40; @Test public void iconResources_correctResources() { diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java index 9263ffdb8c7..29e9cf9db07 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -34,10 +34,12 @@ import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.app.Flags; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.UserInfo; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; @@ -102,11 +104,12 @@ public final class ZenModeAppsLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); mPreference = new TestableCircularIconsPreference(mContext); - when(mApplicationsState.newSession(any(), any())).thenReturn(mSession); + mController = new ZenModeAppsLinkPreferenceController( mContext, "controller_key", mock(Fragment.class), mApplicationsState, - mZenModesBackend, mHelperBackend); + mZenModesBackend, mHelperBackend, + /* appIconRetriever= */ appInfo -> new ColorDrawable()); // Ensure the preference view is bound & measured (needed to add child ImageViews). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), @@ -163,7 +166,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) .isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment"); assertThat(launcherIntent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, - -1)).isEqualTo(0); + -1)).isEqualTo(SettingsEnums.ZEN_PRIORITY_MODE); Bundle bundle = launcherIntent.getBundleExtra( SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); @@ -295,6 +298,89 @@ public final class ZenModeAppsLinkPreferenceControllerTest { verify(mSession, times(2)).rebuild(any(), any(), eq(false)); } + @Test + public void updateState_noneToPriority_loadsBypassingAppsAndListensForChanges() { + ZenMode zenModeWithNone = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build()) + .build(); + ZenMode zenModeWithPriority = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build()) + .build(); + ArrayList appEntries = new ArrayList<>(); + appEntries.add(createAppEntry("test", mContext.getUserId())); + when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false)) + .thenReturn(List.of("test")); + + mController.updateState(mPreference, zenModeWithNone); + + assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + verifyNoMoreInteractions(mApplicationsState); + verifyNoMoreInteractions(mSession); + + mController.updateState(mPreference, zenModeWithPriority); + + verify(mApplicationsState).newSession(any(), any()); + verify(mSession).rebuild(any(), any(), anyBoolean()); + mController.mAppSessionCallbacks.onRebuildComplete(appEntries); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + } + + @Test + public void updateState_priorityToNone_clearsBypassingAppsAndStopsListening() { + ZenMode zenModeWithNone = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build()) + .build(); + ZenMode zenModeWithPriority = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build()) + .build(); + ArrayList appEntries = new ArrayList<>(); + appEntries.add(createAppEntry("test", mContext.getUserId())); + when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false)) + .thenReturn(List.of("test")); + + mController.updateState(mPreference, zenModeWithPriority); + + verify(mApplicationsState).newSession(any(), any()); + verify(mSession).rebuild(any(), any(), anyBoolean()); + mController.mAppSessionCallbacks.onRebuildComplete(appEntries); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + + mController.updateState(mPreference, zenModeWithNone); + + assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + verify(mSession).deactivateSession(); + verifyNoMoreInteractions(mSession); + verifyNoMoreInteractions(mApplicationsState); + + // An errant callback (triggered by onResume and received asynchronously after + // updateState()) is ignored. + mController.mAppSessionCallbacks.onRebuildComplete(appEntries); + + assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + } + + @Test + public void updateState_priorityToNoneToPriority_restartsListening() { + ZenMode zenModeWithNone = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build()) + .build(); + ZenMode zenModeWithPriority = new TestModeBuilder() + .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build()) + .build(); + + mController.updateState(mPreference, zenModeWithPriority); + verify(mApplicationsState).newSession(any(), any()); + verify(mSession).rebuild(any(), any(), anyBoolean()); + + mController.updateState(mPreference, zenModeWithNone); + verifyNoMoreInteractions(mApplicationsState); + verify(mSession).deactivateSession(); + + mController.updateState(mPreference, zenModeWithPriority); + verifyNoMoreInteractions(mApplicationsState); + verify(mSession).activateSession(); + } + @Test public void testNoCrashIfAppsReadyBeforeRuleAvailable() { mController.mAppSessionCallbacks.onLoadEntriesCompleted(); diff --git a/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java b/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java index a47703c07c3..bec49e1933b 100644 --- a/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java +++ b/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java @@ -80,6 +80,17 @@ public class UserCapabilitiesTest { assertThat(userCapabilities.mDisallowSwitchUser).isFalse(); } + @Test + public void changeAdminStatus_updateUserCapabilities_mIsAdminGetsUpdated() { + mUserManager.setIsAdminUser(false); + UserCapabilities userCapabilities = UserCapabilities.create(mContext); + assertThat(userCapabilities.isAdmin()).isFalse(); + + mUserManager.setIsAdminUser(true); + userCapabilities.updateAddUserCapabilities(mContext); + assertThat(userCapabilities.mIsAdmin).isTrue(); + } + @Test public void userSwitchEnabled_off() { mUserManager.setUserSwitcherEnabled(false); diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt index 4137de494bc..0cbfe02dd42 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt @@ -17,20 +17,21 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.AccessNetworkConstants -import android.telephony.NetworkRegistrationInfo -import android.telephony.ServiceState -import android.telephony.TelephonyManager +import android.os.PersistableBundle +import android.telephony.* +import android.telephony.satellite.SatelliteManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.network.telephony.scan.NetworkScanRepositoryTest import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.stub +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class NetworkSelectRepositoryTest { @@ -49,8 +50,16 @@ class NetworkSelectRepositoryTest { on { serviceState } doReturn mockServiceState } + private val mockSatelliteManager = mock { + on { getSatellitePlmnsForCarrier(anyInt()) } doReturn SatellitePlmns + } + + private var mockCarrierConfigManager = mock() + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + on { getSystemService(SatelliteManager::class.java) } doReturn mockSatelliteManager + on { getSystemService(CarrierConfigManager::class.java) } doReturn mockCarrierConfigManager } private val repository = NetworkSelectRepository(context, SUB_ID) @@ -105,6 +114,14 @@ class NetworkSelectRepositoryTest { on { forbiddenPlmns } doReturn arrayOf(FORBIDDEN_PLMN) } + val config = PersistableBundle() + config.putBoolean( + CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, + false) + whenever(mockCarrierConfigManager.getConfigForSubId( + SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL)) + .thenReturn(config) + val info = repository.getNetworkRegistrationInfo() assertThat(info).isEqualTo( @@ -115,9 +132,76 @@ class NetworkSelectRepositoryTest { ) } + @Test + fun getNetworkRegistrationInfo_filterSatellitePlmn() { + + val info1 = createTestNetworkRegistrationInfo("310", "260") + val info2 = createTestNetworkRegistrationInfo("310", "261") + val satelliteInfo = createTestNetworkRegistrationInfo(satelliteMcc, satelliteMnc) + val registrationInfos = listOf(info1, info2, satelliteInfo) + val filteredRegistrationInfos = listOf(info1, info2) + + mockServiceState.stub { + on { + getNetworkRegistrationInfoListForTransportType( + AccessNetworkConstants.TRANSPORT_TYPE_WWAN + ) + } doReturn registrationInfos + } + mockTelephonyManager.stub { + on { forbiddenPlmns } doReturn arrayOf(FORBIDDEN_PLMN) + } + + // disable satellite plmn filter + var config = PersistableBundle() + config.putBoolean( + CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, + false) + whenever(mockCarrierConfigManager.getConfigForSubId( + SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL)) + .thenReturn(config) + + var infoList = repository.getNetworkRegistrationInfo() + + assertThat(infoList).isEqualTo( + NetworkSelectRepository.NetworkRegistrationAndForbiddenInfo( + networkList = registrationInfos, + forbiddenPlmns = listOf(FORBIDDEN_PLMN), + ) + ) + + // enable satellite plmn filter + config = PersistableBundle() + config.putBoolean( + CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, + true) + whenever(mockCarrierConfigManager.getConfigForSubId( + SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL)) + .thenReturn(config) + + infoList = repository.getNetworkRegistrationInfo() + + assertThat(infoList).isEqualTo( + NetworkSelectRepository.NetworkRegistrationAndForbiddenInfo( + networkList = filteredRegistrationInfos, + forbiddenPlmns = listOf(FORBIDDEN_PLMN), + ) + ) + } + private companion object { const val SUB_ID = 1 val NetworkRegistrationInfos = listOf(NetworkRegistrationInfo.Builder().build()) const val FORBIDDEN_PLMN = "Forbidden PLMN" + const val satelliteMcc = "310" + const val satelliteMnc = "810" + val SatellitePlmns = listOf(satelliteMcc + satelliteMnc) + + fun createTestNetworkRegistrationInfo(mcc: String, mnc: String): NetworkRegistrationInfo { + val cellInfo = CellIdentityLte(0, 0, 0, 0, IntArray(2) { 0 }, + 0, mcc, mnc, "", "", emptyList(), null) + + return NetworkRegistrationInfo.Builder().setCellIdentity(cellInfo).build() + } } } diff --git a/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java b/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java index 326627a6247..34878e1ef2c 100644 --- a/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/deviceinfo/BuildNumberPreferenceControllerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +33,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.Flags; import android.os.Looper; import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -193,6 +195,7 @@ public class BuildNumberPreferenceControllerTest { @Test @UiThreadTest + @RequiresFlagsDisabled(Flags.FLAG_MANDATORY_BIOMETRICS) public void onActivityResult_confirmPasswordRequestCompleted_enableDevPref() { when(mUserManager.isAdminUser()).thenReturn(true); @@ -206,7 +209,6 @@ public class BuildNumberPreferenceControllerTest { } @Test - @UiThreadTest @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) public void onActivityResult_confirmPasswordRequestCompleted_launchBiometricPrompt() { when(mUserManager.isAdminUser()).thenReturn(true); @@ -225,6 +227,45 @@ public class BuildNumberPreferenceControllerTest { eq(BuildNumberPreferenceController.REQUEST_IDENTITY_CHECK_FOR_DEV_PREF)); } + @Test + @UiThreadTest + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void onActivityResult_confirmPasswordRequestCompleted_mandatoryBiometricsError() { + when(mUserManager.isAdminUser()).thenReturn(true); + when(mBiometricManager.canAuthenticate(mContext.getUserId(), + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE); + + final boolean activityResultHandled = mController.onActivityResult( + BuildNumberPreferenceController.REQUEST_CONFIRM_PASSWORD_FOR_DEV_PREF, + Activity.RESULT_OK, + null); + + assertThat(activityResultHandled).isTrue(); + verify(mFragment, never()).startActivityForResult(any(), + eq(BuildNumberPreferenceController.REQUEST_IDENTITY_CHECK_FOR_DEV_PREF)); + } + + @Test + @UiThreadTest + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void onActivityResult_confirmPasswordRequestCompleted_lockoutError() { + when(mUserManager.isAdminUser()).thenReturn(true); + when(mBiometricManager.canAuthenticate(mContext.getUserId(), + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_LOCKOUT); + + final boolean activityResultHandled = mController.onActivityResult( + BuildNumberPreferenceController.REQUEST_CONFIRM_PASSWORD_FOR_DEV_PREF, + Activity.RESULT_OK, + null); + + assertThat(activityResultHandled).isTrue(); + verify(mFragment, never()).startActivityForResult(any(), + eq(BuildNumberPreferenceController.REQUEST_IDENTITY_CHECK_FOR_DEV_PREF)); + assertThat(DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext)).isFalse(); + } + @Test public void onActivityResult_confirmBiometricAuthentication_enableDevPref() { when(mUserManager.isAdminUser()).thenReturn(true);