Snap for 12235414 from d80647ea37 to 24Q4-release

Change-Id: I5b7dfb140d05f18aca876edf8326f1844b6cbbca
This commit is contained in:
Android Build Coastguard Worker
2024-08-15 23:21:06 +00:00
83 changed files with 2355 additions and 488 deletions

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q414,520 367,473Q320,426 320,360Q320,294 367,247Q414,200 480,200Q546,200 593,247Q640,294 640,360Q640,426 593,473Q546,520 480,520ZM160,840L160,728Q160,695 177,666Q194,637 224,622Q275,596 339,578Q403,560 480,560Q557,560 621,578Q685,596 736,622Q766,637 783,666Q800,695 800,728L800,840L160,840ZM240,760L720,760L720,728Q720,717 714.5,708Q709,699 700,694Q664,676 607.5,658Q551,640 480,640Q409,640 352.5,658Q296,676 260,694Q251,699 245.5,708Q240,717 240,728L240,760ZM480,440Q513,440 536.5,416.5Q560,393 560,360Q560,327 536.5,303.5Q513,280 480,280Q447,280 423.5,303.5Q400,327 400,360Q400,393 423.5,416.5Q447,440 480,440ZM39,200L39,120Q56,120 70,113.5Q84,107 95,96Q106,85 112,71Q118,57 118,40L199,40Q199,73 186.5,102Q174,131 152,153Q130,175 101,187.5Q72,200 39,200ZM39,361L39,281Q90,281 133.5,262Q177,243 209,210Q241,177 260,133.5Q279,90 279,40L360,40Q360,106 335,164.5Q310,223 266,267Q222,311 164,336Q106,361 39,361ZM920,361Q854,361 795.5,336Q737,311 693,267Q649,223 624,164.5Q599,106 599,40L679,40Q679,90 698,133.5Q717,177 750,210Q783,243 826.5,262Q870,281 920,281L920,361ZM920,200Q887,200 858,187.5Q829,175 807,153Q785,131 772.5,102Q760,73 760,40L840,40Q840,57 846.5,71Q853,85 864,96Q875,107 889,113.5Q903,120 920,120L920,200ZM480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360ZM480,760L480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760L480,760L480,760Z" />
</vector>

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="@android:color/white"
android:pathData="M920,401Q848,401 782,373.5Q716,346 665,295Q614,244 586.5,178Q559,112 559,40L639,40Q639,97 660,148Q681,199 721,239Q761,279 812,300.5Q863,322 920,322L920,401ZM920,242Q879,242 842.5,227Q806,212 777,183Q748,154 733,117.5Q718,81 718,40L797,40Q797,65 806.5,87.5Q816,110 833,127Q850,144 872.5,153Q895,162 920,162L920,242ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
</vector>

View File

@@ -0,0 +1,26 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
android:fillColor="@android:color/white"
android:pathData="M750,550L806,494Q766,454 743.5,402.5Q721,351 721,294Q721,237 743.5,186Q766,135 806,95L750,37Q699,88 670,155Q641,222 641,294Q641,366 670,432.5Q699,499 750,550ZM862,436L918,380Q901,363 891,341Q881,319 881,294Q881,269 891,247Q901,225 918,208L862,151Q833,180 817,216Q801,252 801,293Q801,334 817,371Q833,408 862,436ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
</vector>

View File

@@ -61,6 +61,7 @@
<!-- Circular icons (32dp) will be ImageViews under this LinearLayout -->
<LinearLayout
android:id="@+id/circles_container"
android:importantForAccessibility="noHideDescendants"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"

View File

@@ -1527,54 +1527,104 @@
<item>2</item>
</string-array>
<!-- TODO: b/333901673 - Get full icon list. -->
<array name="zen_mode_icon_options" translatable="false">
<item>@*android:drawable/ic_zen_mode_type_bedtime</item>
<item>@*android:drawable/ic_zen_mode_type_driving</item>
<item>@*android:drawable/ic_zen_mode_type_immersive</item>
<!-- Work/Study/Meetings -->
<item>@*android:drawable/ic_zen_mode_icon_work</item> <!-- Briefcase -->
<item>@*android:drawable/ic_zen_mode_icon_classical_building</item>
<item>@*android:drawable/ic_zen_mode_icon_apartment_building</item>
<item>@*android:drawable/ic_zen_mode_icon_speech_bubble</item>
<item>@*android:drawable/ic_zen_mode_icon_group_of_people</item>
<item>@*android:drawable/ic_zen_mode_icon_lightbulb</item>
<item>@*android:drawable/ic_zen_mode_type_schedule_calendar</item>
<item>@*android:drawable/ic_zen_mode_type_schedule_time</item>
<item>@*android:drawable/ic_zen_mode_icon_beach</item>
<item>@*android:drawable/ic_zen_mode_icon_camping</item>
<item>@*android:drawable/ic_zen_mode_type_theater</item>
<item>@*android:drawable/ic_zen_mode_icon_gaming</item>
<!-- Sports -->
<item>@*android:drawable/ic_zen_mode_icon_running</item>
<item>@*android:drawable/ic_zen_mode_icon_golf</item>
<item>@*android:drawable/ic_zen_mode_icon_gym</item>
<item>@*android:drawable/ic_zen_mode_icon_ball_sports</item>
<item>@*android:drawable/ic_zen_mode_icon_martial_arts</item>
<item>@*android:drawable/ic_zen_mode_icon_swimming</item>
<item>@*android:drawable/ic_zen_mode_icon_hiking</item>
<item>@*android:drawable/ic_zen_mode_icon_golf</item>
<item>@*android:drawable/ic_zen_mode_icon_ball_sports</item>
<item>@*android:drawable/ic_zen_mode_icon_martial_arts</item>
<!-- Leisure -->
<item>@*android:drawable/ic_zen_mode_icon_gaming</item>
<item>@*android:drawable/ic_zen_mode_icon_palette</item>
<item>@*android:drawable/ic_zen_mode_icon_snowflake</item>
<item>@*android:drawable/ic_zen_mode_icon_beach</item>
<item>@*android:drawable/ic_zen_mode_icon_workshop</item>
<item>@*android:drawable/ic_zen_mode_icon_work</item>
<item>@*android:drawable/ic_zen_mode_type_other</item>
<item>@*android:drawable/ic_zen_mode_type_unknown</item>
<item>@*android:drawable/ic_zen_mode_type_managed</item>
<item>@*android:drawable/ic_zen_mode_icon_camping</item>
<item>@*android:drawable/ic_zen_mode_type_theater</item> <!-- Film reel -->
<item>@*android:drawable/ic_zen_mode_icon_book</item>
<!-- Wellbeing -->
<item>@*android:drawable/ic_zen_mode_type_unknown</item> <!-- Lotus flower -->
<item>@*android:drawable/ic_zen_mode_type_immersive</item>
<item>@*android:drawable/ic_zen_mode_icon_headphones</item>
<item>@*android:drawable/ic_zen_mode_icon_tv</item>
<!-- Other activities -->
<item>@*android:drawable/ic_zen_mode_icon_train</item>
<item>@*android:drawable/ic_zen_mode_type_driving</item> <!-- Car -->
<item>@*android:drawable/ic_zen_mode_icon_croissant</item>
<item>@*android:drawable/ic_zen_mode_icon_fork_and_knife</item>
<item>@*android:drawable/ic_zen_mode_icon_shopping_cart</item>
<item>@*android:drawable/ic_zen_mode_icon_child</item>
<item>@*android:drawable/ic_zen_mode_icon_rabbit</item>
<item>@*android:drawable/ic_zen_mode_icon_animal_paw</item>
<!-- Generic / abstract -->
<item>@*android:drawable/ic_zen_mode_type_managed</item> <!-- Account -->
<item>@*android:drawable/ic_zen_mode_type_other</item> <!-- Star -->
<item>@*android:drawable/ic_zen_mode_icon_heart</item>
<item>@*android:drawable/ic_zen_mode_icon_house</item>
<item>@*android:drawable/ic_zen_mode_type_bedtime</item> <!-- Moon -->
<item>@*android:drawable/ic_zen_mode_type_schedule_time</item>
</array>
<!-- TODO: b/333901673 - Complete list -->
<!-- Descriptions of the icons in zen_mode_icon_options. Should describe the associated image
[CHAR LIMIT=NONE] -->
<string-array name="zen_mode_icon_options_descriptions">
<item>Half-moon</item>
<item>Car</item>
<item>Person\'s mind</item>
<!-- Work/Study/Meetings -->
<item>Briefcase</item>
<item>Classical building</item>
<item>Apartment building</item>
<item>Speech bubble</item>
<item>Group of people</item>
<item>Lightbulb</item>
<item>Calendar</item>
<item>Clock</item>
<item>Beach umbrella</item>
<item>Tent</item>
<item>Film reel</item>
<item>Game controller</item>
<!-- Sports -->
<item>Person running</item>
<item>Golf</item>
<item>Gym dumbbell</item>
<item>Person throwing ball</item>
<item>Person kicking</item>
<item>Swimming</item>
<item>Person hiking</item>
<item>Golf</item>
<item>Person throwing ball</item>
<item>Person kicking</item>
<!-- Leisure -->
<item>Game controller</item>
<item>Artist color palette</item>
<item>Snowflake</item>
<item>Beach umbrella</item>
<item>Workshop tools</item>
<item>Briefcase</item>
<item>Star</item>
<item>Tent</item>
<item>Film reel</item>
<item>Book</item>
<!-- Wellbeing -->
<item>Lotus flower</item>
<item>Person\'s mind</item>
<item>Headphones</item>
<item>TV</item>
<!-- Other activities -->
<item>Train</item>
<item>Car</item>
<item>Croissant</item>
<item>Fork and knife</item>
<item>Shopping cart</item>
<item>Child</item>
<item>Rabbit</item>
<item>Animal paw</item>
<!-- Generic / abstract -->
<item>Supervisor</item>
<item>Star</item>
<item>Heart</item>
<item>House</item>
<item>Half-moon</item>
<item>Clock</item>
</string-array>
<!-- Packages that will not show Display over other apps permission -->

View File

@@ -7946,6 +7946,18 @@
<!-- Sound: Footer hyperlink text to launch the Connected devices settings page. [CHAR LIMIT=NONE]-->
<string name="spatial_audio_footer_learn_more_text">Connected devices settings</string>
<!-- Bluetooth device details: spatial audio multi-toggle title. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_title">Spatial Audio</string>
<!-- Bluetooth device details: spatial audio is off. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_off">Off</string>
<!-- Bluetooth device details: spatial audio is on. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_on">Off</string>
<!-- Bluetooth device details: head tracking is on. [CHAR LIMIT=20]-->
<string name="spatial_audio_multi_toggle_head_tracking_on">Off</string>
<!-- Zen Modes: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
<string name="zen_mode_settings_schedules_summary">
{count, plural,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <T extends AbstractPreferenceController> void getController(Class<T> clazz,
Consumer<T> 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<String> 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<AbstractPreferenceController> createPreferenceControllers(Context context) {
if (Flags.enableBluetoothDeviceDetailsPolish()) {
mFormatter =
FeatureFactory.getFeatureFactory()
.getBluetoothFeatureProvider()
.getDeviceDetailsFragmentFormatter(
requireContext(), this, mBluetoothAdapter, mCachedDevice);
}
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
if (mCachedDevice != null) {

View File

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

View File

@@ -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<ComponentName> getRelatedTools() {
return null;
}
@Override
public Spatializer getSpatializer(Context context) {
AudioManager audioManager = context.getSystemService(AudioManager.class);
return audioManager.getSpatializer();
}
@Override
public List<Preference> getBluetoothExtraOptions(Context context,
CachedBluetoothDevice device) {
return ImmutableList.of();
}
@Override
public Set<String> getInvisibleProfilePreferenceKeys(
Context context, BluetoothDevice bluetoothDevice) {
return ImmutableSet.of();
}
}

View File

@@ -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<ComponentName>? {
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<Preference>? {
return ImmutableList.of<Preference>()
}
override fun getInvisibleProfilePreferenceKeys(
context: Context,
bluetoothDevice: BluetoothDevice
): Set<String> {
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)
}
}

View File

@@ -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<DeviceSettingModel?>
}
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<Unit>()
override fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
): Flow<DeviceSettingModel?> =
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
}
}

View File

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

View File

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

View File

@@ -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<DeviceSettingLayoutRow>)
/** Represent a row in the layout. */
data class DeviceSettingLayoutRow(val settingIds: Flow<List<Int>>)

View File

@@ -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<String>?
/** 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<String>? = runBlocking {
viewModel
.getItems()
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
?.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<DeviceSettingConfigItemModel.BuiltinItem>()
.associateBy({ it.preferenceKey }, { it.settingId })
val settingIdToXmlPreferences: MutableMap<Int, Preference> = 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<DeviceSettingModel>())
} 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<DeviceSettingModel.MultiTogglePreference>())
}
}
}
@Composable
private fun buildMultiTogglePreference(prefs: List<DeviceSettingModel.MultiTogglePreference>) {
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"
}
}

View File

@@ -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<DeviceSettingConfigItemModel>? = items.await()?.mainItems
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
@DeviceSettingId settingId: Int
): Flow<DeviceSettingModel?> {
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<DeviceSettingConfigItemModel.AppProvidedItem>()
.associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
val configDeviceSetting =
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds =
combine(configDeviceSetting) { settings ->
val positionMapping = mutableMapOf<Int, List<Int>>()
var multiToggleSettingIds: MutableList<Int>? = 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 <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(
deviceSettingRepository, spatialAudioInteractor, cachedDevice)
as T
}
}
companion object {
private const val TAG = "BluetoothDeviceDetailsViewModel"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<NetworkRegistrationInfo>,
@@ -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<String> {
return telephonyManager.forbiddenPlmns?.toList() ?: emptyList()
}
/**
* Update satellite PLMNs from the satellite framework.
*/
private fun getSatellitePlmns(): List<String> {
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();
}
}
}

View File

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

View File

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

View File

@@ -49,7 +49,9 @@ public class CircularIconsPreference extends RestrictedPreference {
private static final float DISABLED_ITEM_ALPHA = 0.3f;
record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) { }
record LoadedIcons(ImmutableList<Drawable> 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();
}
}
);

View File

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

View File

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

View File

@@ -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<ApplicationInfo, Drawable> 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<ApplicationInfo, Drawable> 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<AppEntry> 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<AppEntry> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 / <Calendar name>
preference.setIcon(

View File

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

View File

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

View File

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

View File

@@ -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<? extends DashboardFragment> fragmentClass, String modeId,
int sourceMetricsCategory) {

View File

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

View File

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

View File

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

View File

@@ -570,7 +570,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
* <li>OR multiple admin support is NOT enabled.</li>
* <li>OR the <b>current</b> user has DISALLOW_GRANT_ADMIN restriction applied</li>
*
* <li>OR the <b>target</b> user ('mUserInfo') is a main user OR a guest user.</li>
* <li>OR the <b>target</b> user ('mUserInfo') is a main user</li>
* <li>OR the <b>target</b> user ('mUserInfo') is not of type
* {@link UserManager#USER_TYPE_FULL_SECONDARY}</li>
* <li>OR the <b>target</b> user ('mUserInfo') has DISALLOW_GRANT_ADMIN restriction.</li>
* </ul>
*
@@ -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());

View File

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

View File

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

View File

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

View File

@@ -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<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(
Intent.class);
ArgumentCaptor<Intent> 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());
}

View File

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

View File

@@ -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<Context>())
`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?>): 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,
)
}
}

View File

@@ -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<Context>()
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<Preference> {
val prefs = mutableListOf<Preference>()
for (i in 0..<fragment.preferenceScreen.preferenceCount) {
prefs.add(fragment.preferenceScreen.getPreference(i))
}
return prefs
}
class TestFragment(context: Context) : DashboardFragment() {
private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
init {
mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
}
public override fun getPreferenceScreenResId(): Int = 0
override fun getLogTag(): String = "TestLogTag"
override fun getPreferenceScreen(): PreferenceScreen {
return mPreferenceManager.preferenceScreen
}
override fun getMetricsCategory(): Int = 0
override fun getPreferenceManager(): PreferenceManager {
return mPreferenceManager
}
}
private companion object {}
}

View File

@@ -0,0 +1,258 @@
/*
* 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 android.bluetooth.BluetoothAdapter
import android.content.Context
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
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.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
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 BluetoothDeviceDetailsViewModelTest {
@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 underTest: BluetoothDeviceDetailsViewModel
private lateinit var featureFactory: FakeFeatureFactory
private val testScope = TestScope()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
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<List<Int>> {
var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
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")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<SatelliteManager> {
on { getSatellitePlmnsForCarrier(anyInt()) } doReturn SatellitePlmns
}
private var mockCarrierConfigManager = mock<CarrierConfigManager>()
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()
}
}
}

View File

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