Snap for 12235414 from d80647ea37 to 24Q4-release
Change-Id: I5b7dfb140d05f18aca876edf8326f1844b6cbbca
This commit is contained in:
26
res/drawable/ic_head_tracking.xml
Normal file
26
res/drawable/ic_head_tracking.xml
Normal 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>
|
||||
26
res/drawable/ic_spatial_audio.xml
Normal file
26
res/drawable/ic_spatial_audio.xml
Normal 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>
|
||||
26
res/drawable/ic_spatial_audio_off.xml
Normal file
26
res/drawable/ic_spatial_audio_off.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal file
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal 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 -> {}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>>)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user