Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d51dfdc..98cb31a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2283,6 +2283,21 @@
android:value="true" />
</activity>
+ <activity
+ android:name="Settings$AccessibilityEditShortcutsActivity"
+ android:label="@string/accessibility_settings"
+ android:exported="true"
+ android:permission="android.permission.MANAGE_ACCESSIBILITY">
+ <intent-filter android:priority="1">
+ <action android:name="android.settings.ACCESSIBILITY_SHORTCUT_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment" />
+ <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+ android:value="@string/menu_key_accessibility"/>
+ </activity>
+
<activity android:name=".accessibility.AccessibilitySettingsForSetupWizardActivity"
android:icon="@drawable/ic_accessibility_suggestion"
android:label="@string/vision_settings_title"
diff --git a/aconfig/accessibility/accessibility_flags.aconfig b/aconfig/accessibility/accessibility_flags.aconfig
index 1c03a27..ad770fb 100644
--- a/aconfig/accessibility/accessibility_flags.aconfig
+++ b/aconfig/accessibility/accessibility_flags.aconfig
@@ -9,6 +9,19 @@
bug: "300302098"
}
+flag {
+ name: "enable_hearing_aid_preset_control"
+ namespace: "accessibility"
+ description: "Allows users to control hearing aid preset in the Bluetooth device details page."
+ bug: "300015207"
+}
+
+flag {
+ name: "enable_hearing_aid_volume_offset_control"
+ namespace: "accessibility"
+ description: "Allows users to control hearing aid volume offset in the Bluetooth device details page."
+ bug: "301198830"
+}
flag {
name: "remove_qs_tooltip_in_suw"
diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml
index e6d1c32..d402dc3 100644
--- a/res/layout/qrcode_scanner_fragment.xml
+++ b/res/layout/qrcode_scanner_fragment.xml
@@ -17,7 +17,6 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -26,36 +25,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3"
- android:layout_marginBottom="35dp">
+ android:layout_marginBottom="55dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:paddingStart="40dp"
+ android:paddingEnd="40dp"
android:layout_gravity="bottom"
android:gravity="center"
android:orientation="vertical">
- <ImageView
- android:src="@drawable/ic_qr_code_scanner"
- android:tint="?androidprv:attr/materialColorPrimaryContainer"
- android:layout_width="@dimen/qrcode_icon_size"
- android:layout_height="@dimen/qrcode_icon_size"
- android:contentDescription="@null"/>
-
<TextView
style="@style/QrCodeScanner"
- android:textSize="24sp"
- android:text="@string/bluetooth_find_broadcast_button_scan"
+ android:text="Scan an audio stream QR code to listen with the active LE device"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="19dp"/>
-
- <TextView
- style="@style/QrCodeScanner"
- android:text="@string/bt_le_audio_scan_qr_code_scanner"
- android:gravity="center"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"/>
+ android:layout_marginTop="20dp"/>
</LinearLayout>
</LinearLayout>
diff --git a/res/values-eu/arrays.xml b/res/values-eu/arrays.xml
index 45ddcb6..3a4f2ef 100644
--- a/res/values-eu/arrays.xml
+++ b/res/values-eu/arrays.xml
@@ -196,7 +196,7 @@
<item msgid="617344340943430125">"bolumen nagusia"</item>
<item msgid="1249691739381713634">"ahotsaren bolumena"</item>
<item msgid="6485000384018554920">"tonuaren bolumena"</item>
- <item msgid="3378000878531336372">"multimedia-elementuen bolumena"</item>
+ <item msgid="3378000878531336372">"multimedia-edukiaren bolumena"</item>
<item msgid="5272927168355895681">"alarmaren bolumena"</item>
<item msgid="4422070755065530548">"jakinarazpenen bolumena"</item>
<item msgid="3250654589277825306">"Bluetooth bidezko audioaren bolumena"</item>
@@ -263,7 +263,7 @@
<item msgid="745291221457314879">"Bolumen nagusia"</item>
<item msgid="4722479281326245754">"Ahotsaren bolumena"</item>
<item msgid="6749550886745567276">"Tonuaren bolumena"</item>
- <item msgid="2218685029915863168">"Multimedia-elementuen bolumena"</item>
+ <item msgid="2218685029915863168">"Multimedia-edukiaren bolumena"</item>
<item msgid="4266577290496513640">"Alarmaren bolumena"</item>
<item msgid="8608084169623998854">"Jakinarazpenen bolumena"</item>
<item msgid="7948784184567841794">"Bluetooth bidezko audioaren bolumena"</item>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 357818c..0e35fed 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -233,6 +233,26 @@
<!-- Bluetooth Settings -->
+ <!-- Bluetooth developer settings: Bluetooth LE Audio modes -->
+ <string-array name="bluetooth_leaudio_mode">
+ <!-- Do not translate. -->
+ <item>Disabled</item>
+ <!-- Do not translate. -->
+ <item>Unicast</item>
+ <!-- Do not translate. -->
+ <item>Unicast and Broadcast</item>
+ </string-array>
+
+ <!-- Values for Bluetooth LE Audio mode -->
+ <string-array name="bluetooth_leaudio_mode_values" translatable="false">
+ <!-- Do not translate. -->
+ <item>disabled</item>
+ <!-- Do not translate. -->
+ <item>unicast</item>
+ <!-- Do not translate. -->
+ <item>broadcast</item>
+ </string-array>
+
<!-- Bluetooth developer settings: Titles for maximum number of connected audio devices -->
<string-array name="bluetooth_max_connected_audio_devices">
<item>Use System Default: <xliff:g id="default_bluetooth_max_connected_audio_devices">%1$d</xliff:g></item>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 01979b2..b760f68 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -249,7 +249,8 @@
<string name="bluetooth_disable_leaudio">Disable Bluetooth LE audio</string>
<!-- Summary of toggle for disabling Bluetooth LE audio [CHAR LIMIT=none]-->
<string name="bluetooth_disable_leaudio_summary">Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities.</string>
-
+ <!-- Setting toggle title for switch Bluetooth LE Audio mode. [CHAR LIMIT=40] -->
+ <string name="bluetooth_leaudio_mode">Bluetooth LE Audio mode</string>
<!-- Setting toggle title for enabling Bluetooth LE Audio toggle in Device Details. [CHAR LIMIT=40] -->
<string name="bluetooth_show_leaudio_device_details">Show LE audio toggle in Device Details</string>
@@ -1743,7 +1744,7 @@
<string name="bluetooth_pairing_request">Pair with <xliff:g id="device_name">%1$s</xliff:g>?</string>
<!-- Message when a bluetooth device from a coordinated set is bonding late. [CHAR LIMIT=NONE] -->
- <string name="bluetooth_pairing_group_late_bonding">Add new member to the existing coordinated set</string>
+ <string name="bluetooth_pairing_group_late_bonding">Confirm to add the second piece of your audio device</string>
<!-- Message when bluetooth is informing the user of the pairing key. [CHAR LIMIT=NONE] -->
<string name="bluetooth_pairing_key_msg">Bluetooth pairing code</string>
@@ -1766,7 +1767,7 @@
<string name="bluetooth_enter_passkey_other_device">You may also need to type this passkey on the other device.</string>
<!-- Pairing dialog text to remind user the pairing including all of the devices in a coordinated set. [CHAR LIMIT=NONE] -->
- <string name="bluetooth_paring_group_msg">Confirm to pair with the coordinated set</string>
+ <string name="bluetooth_paring_group_msg">Confirm to pair with the audio device</string>
<!-- Checkbox message in pairing dialogs. [CHAR LIMIT=NONE] -->
<string name="bluetooth_pairing_shares_phonebook">Allow access to your contacts and call history</string>
@@ -4311,6 +4312,10 @@
<string name="bounce_keys">Bounce keys</string>
<!-- Summary text for the 'Bounce keys' preference sub-screen. [CHAR LIMIT=100] -->
<string name="bounce_keys_summary">Enable Bounce keys for physical keyboard accessibility</string>
+ <!-- Title for the 'Slow keys' preference switch. [CHAR LIMIT=35] -->
+ <string name="slow_keys">Slow keys</string>
+ <!-- Summary text for the 'Slow keys' preference sub-screen. [CHAR LIMIT=100] -->
+ <string name="slow_keys_summary">Enable Slow keys for physical keyboard accessibility</string>
<!-- Title for the 'Sticky keys' preference switch. [CHAR LIMIT=35] -->
<string name="sticky_keys">Sticky keys</string>
<!-- Summary text for the 'Sticky keys' preference sub-screen. [CHAR LIMIT=100] -->
@@ -9619,10 +9624,6 @@
<string name="permit_voice_activation_apps">Allow voice activation</string>
<!-- Description for a setting which controls whether an app can be voice activated [CHAR LIMIT=NONE] -->
<string name ="allow_voice_activation_apps_description">Voice activation turns-on approved apps, hands-free, using voice command. Built-in adaptive sensing ensures data stays private only to you.\n\n<a href="">More about protected adaptive sensing</a></string>
- <!-- Label for a setting which controls whether an app can receive sandboxed detection training data [CHAR LIMIT=NONE] -->
- <string name = "permit_receive_sandboxed_detection_training_data">Improve voice activation</string>
- <!-- Description for a setting which controls whether an app can receive sandboxed detection training data [CHAR LIMIT=NONE] -->
- <string name= "receive_sandboxed_detection_training_data_description">This device uses private intelligence to improve the voice activation model. Apps can receive summarized updates that are aggregated across many users to maintain privacy while improving the model for everyone.\n\n<a href="">More about private intelligence</a></string>
<!-- Manage full screen intent permission title [CHAR LIMIT=40] -->
<string name="full_screen_intent_title">Full screen notifications</string>
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d3aad22..5de8dfc 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -45,9 +45,15 @@
<com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
android:key="audio_sharing_stream_name"
- android:summary="********"
- android:title="Stream name"
+ android:title="Name"
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
+
+ <com.android.settings.widget.ValidatedEditTextPreference
+ android:key="audio_sharing_stream_password"
+ android:summary="********"
+ android:title="Password"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
+
<SwitchPreferenceCompat
android:key="audio_sharing_stream_compatibility"
android:title="Improve compatibility"
diff --git a/res/xml/bluetooth_audio_streams.xml b/res/xml/bluetooth_audio_streams.xml
index 95ee710..e7e708e 100644
--- a/res/xml/bluetooth_audio_streams.xml
+++ b/res/xml/bluetooth_audio_streams.xml
@@ -18,23 +18,31 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
- android:title="@string/audio_streams_title">
+ android:title="Find an audio stream">
- <Preference
- android:key="audio_streams_scan_qr_code"
- android:title="@string/bluetooth_find_broadcast_button_scan"
- android:icon="@drawable/ic_add_24dp"
- android:summary="@string/audio_streams_qr_code_summary"
- settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
+ <com.android.settingslib.widget.TopIntroPreference
+ android:key="audio_streams_top_intro"
+ android:title="Listen to a device that's sharing audio or to a nearby Auracast broadcast"
+ settings:searchable="false"/>
<Preference
android:key="audio_streams_active_device"
- android:title="Listen with"
+ android:title="Your audio device"
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" />
<com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
android:key="audio_streams_nearby_category"
- android:title="@string/audio_streams_pref_title"
- settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController" />
+ android:title="Audio streams nearby"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController">
+
+ <Preference
+ android:key="audio_streams_scan_qr_code"
+ android:title="Scan a QR code"
+ android:icon="@drawable/ic_add_24dp"
+ android:summary="Start listening by scanning a stream's QR code"
+ android:order="0"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
+
+ </com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference>
</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml
index 502e55a..024e537 100644
--- a/res/xml/bluetooth_audio_streams_dialog.xml
+++ b/res/xml/bluetooth_audio_streams_dialog.xml
@@ -16,6 +16,7 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -23,70 +24,78 @@
android:id="@+id/dialog_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:paddingStart="25dp"
+ android:paddingEnd="25dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/broadcast_dialog_margin"
+ android:layout_marginBottom="25dp"
android:orientation="vertical">
<ImageView
android:id="@+id/dialog_icon"
- android:layout_width="36dp"
- android:layout_height="36dp"
- android:layout_marginTop="@dimen/broadcast_dialog_icon_margin_top"
- android:layout_marginBottom="@dimen/broadcast_dialog_title_img_margin_top"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_marginTop="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_bt_audio_sharing"/>
<TextView
- style="@style/BroadcastDialogTitleStyle"
android:id="@+id/dialog_title"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="15dp"
android:gravity="center"
android:layout_gravity="center"/>
<TextView
- style="@style/BroadcastDialogBodyStyle"
android:id="@+id/dialog_subtitle"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
+ android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="15dp"
android:gravity="center"
android:layout_gravity="center"
android:visibility="gone"/>
<TextView
- style="@style/BroadcastDialogBodyStyle"
android:id="@+id/dialog_subtitle_2"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="15dp"
android:gravity="center"
android:layout_gravity="center"
android:visibility="gone"/>
</LinearLayout>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/broadcast_dialog_margin"
- android:orientation="horizontal">
+ android:layout_marginBottom="@dimen/broadcast_dialog_margin">
<Button
android:id="@+id/left_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_weight="1"
- android:visibility="invisible"/>
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
<Button
android:id="@+id/right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_marginRight="16dp"
- android:visibility="invisible"/>
- </LinearLayout>
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout>
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml
index c750963..50b1429 100644
--- a/res/xml/bluetooth_audio_streams_qr_code.xml
+++ b/res/xml/bluetooth_audio_streams_qr_code.xml
@@ -36,7 +36,7 @@
android:gravity="start"
android:textSize="15sp"
android:textColor="?android:attr/textColorPrimary"
- android:text="Scan this QR code with another device connected to LE audio headphones to start sharing audio"/>
+ android:text="To listen to this audio stream, other people can connect compatible headphones to their Android device. They can then scan this QR code."/>
<LinearLayout
android:layout_width="match_parent"
@@ -50,6 +50,13 @@
android:layout_width="@dimen/qrcode_size"
android:layout_height="@dimen/qrcode_size"
android:src="@android:color/transparent"/>
+
+ <TextView
+ android:id="@+id/password"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="15sp"
+ android:textColor="?android:attr/textColorPrimary"/>
</LinearLayout>
</LinearLayout>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index d44927f..fb5e280 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -373,6 +373,13 @@
android:title="@string/bluetooth_disable_leaudio"
android:summary="@string/bluetooth_disable_leaudio_summary" />
+ <ListPreference
+ android:key="bluetooth_leaudio_mode"
+ android:title="@string/bluetooth_leaudio_mode"
+ android:summary="@string/summary_placeholder"
+ android:entries="@array/bluetooth_leaudio_mode"
+ android:entryValues="@array/bluetooth_leaudio_mode_values"/>
+
<SwitchPreferenceCompat
android:key="bluetooth_show_leaudio_device_details"
android:title="@string/bluetooth_show_leaudio_device_details"/>
diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml
index 1a8ee08..04f248e 100644
--- a/res/xml/network_provider_internet.xml
+++ b/res/xml/network_provider_internet.xml
@@ -52,7 +52,6 @@
settings:keywords="@string/keywords_more_mobile_networks"
settings:userRestriction="no_config_mobile_networks"
settings:isPreferenceVisible="@bool/config_show_sim_info"
- settings:allowDividerAbove="true"
settings:useAdminDisabledSummary="true"
settings:searchable="@bool/config_show_sim_info"/>
diff --git a/res/xml/physical_keyboard_settings.xml b/res/xml/physical_keyboard_settings.xml
index dc424d1..b95f23e 100644
--- a/res/xml/physical_keyboard_settings.xml
+++ b/res/xml/physical_keyboard_settings.xml
@@ -44,16 +44,22 @@
android:title="@string/keyboard_a11y_category">
<SwitchPreference
- android:key="accessibility_bounce_keys"
- android:title="@string/bounce_keys"
- android:summary="@string/bounce_keys_summary"
- android:defaultValue="false" />
-
- <SwitchPreference
android:key="accessibility_sticky_keys"
android:title="@string/sticky_keys"
android:summary="@string/sticky_keys_summary"
android:defaultValue="false" />
+ <SwitchPreference
+ android:key="accessibility_slow_keys"
+ android:title="@string/slow_keys"
+ android:summary="@string/slow_keys_summary"
+ android:defaultValue="false" />
+
+ <SwitchPreference
+ android:key="accessibility_bounce_keys"
+ android:title="@string/bounce_keys"
+ android:summary="@string/bounce_keys_summary"
+ android:defaultValue="false" />
+
</PreferenceCategory>
</PreferenceScreen>
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 2275c6d..63ce331 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -102,6 +102,7 @@
public static class DevelopmentSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilitySettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityDetailsSettingsActivity extends SettingsActivity { /* empty */ }
+ public static class AccessibilityEditShortcutsActivity extends SettingsActivity { /* empty */ }
public static class CaptioningSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityInversionSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityContrastSettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/accessibility/FontSizeData.java b/src/com/android/settings/accessibility/FontSizeData.java
index 1226d25..096710d 100644
--- a/src/com/android/settings/accessibility/FontSizeData.java
+++ b/src/com/android/settings/accessibility/FontSizeData.java
@@ -25,6 +25,7 @@
import android.provider.Settings;
import com.android.settingslib.R;
+import com.android.window.flags.Flags;
import java.util.Arrays;
import java.util.List;
@@ -38,12 +39,11 @@
FontSizeData(Context context) {
super(context);
-
final Resources resources = getContext().getResources();
final ContentResolver resolver = getContext().getContentResolver();
final List<String> strEntryValues =
Arrays.asList(resources.getStringArray(R.array.entryvalues_font_size));
- setDefaultValue(FONT_SCALE_DEF_VALUE);
+ setDefaultValue(getFontScaleDefValue(resolver));
final float currentScale =
Settings.System.getFloat(resolver, Settings.System.FONT_SCALE, getDefaultValue());
setInitialIndex(fontSizeValueToIndex(currentScale, strEntryValues.toArray(new String[0])));
@@ -78,4 +78,10 @@
}
return indices.length - 1;
}
+
+ private float getFontScaleDefValue(ContentResolver resolver) {
+ return Flags.configurableFontScaleDefault() ? Settings.System.getFloat(resolver,
+ Settings.System.DEFAULT_DEVICE_FONT_SCALE, FONT_SCALE_DEF_VALUE)
+ : FONT_SCALE_DEF_VALUE;
+ }
}
diff --git a/src/com/android/settings/accessibility/PreferredShortcuts.java b/src/com/android/settings/accessibility/PreferredShortcuts.java
index 2c9840d..d4e8e0c 100644
--- a/src/com/android/settings/accessibility/PreferredShortcuts.java
+++ b/src/com/android/settings/accessibility/PreferredShortcuts.java
@@ -19,10 +19,17 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import androidx.annotation.NonNull;
+
+import com.android.internal.accessibility.common.ShortcutConstants;
+import com.android.internal.accessibility.util.ShortcutUtils;
import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
/** Static utility methods relating to {@link PreferredShortcut} */
@@ -81,6 +88,41 @@
}
/**
+ * Update the user preferred shortcut from Settings data
+ *
+ * @param context {@link Context} to access the {@link SharedPreferences}
+ * @param components contains a set of {@link ComponentName} the service or activity. The
+ * string
+ * representation of the ComponentName should be in the format of
+ * {@link ComponentName#flattenToString()}.
+ */
+ public static void updatePreferredShortcutsFromSettings(
+ @NonNull Context context, @NonNull Set<String> components) {
+ final Map<Integer, Set<String>> shortcutTypeToTargets = new ArrayMap<>();
+ for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) {
+ shortcutTypeToTargets.put(
+ shortcutType,
+ ShortcutUtils.getShortcutTargetsFromSettings(
+ context, shortcutType, UserHandle.myUserId()));
+ }
+
+ for (String target : components) {
+ int shortcutTypes = ShortcutConstants.UserShortcutType.DEFAULT;
+ for (Map.Entry<Integer, Set<String>> entry : shortcutTypeToTargets.entrySet()) {
+ if (entry.getValue().contains(target)) {
+ shortcutTypes |= entry.getKey();
+ }
+ }
+
+ if (shortcutTypes != ShortcutConstants.UserShortcutType.DEFAULT) {
+ final PreferredShortcut shortcut = new PreferredShortcut(
+ target, shortcutTypes);
+ PreferredShortcuts.saveUserShortcutType(context, shortcut);
+ }
+ }
+ }
+
+ /**
* Returns a immutable set of {@link PreferredShortcut#toString()} list from
* SharedPreferences.
*/
diff --git a/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java b/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java
index 6666554..a3cbb57 100644
--- a/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java
+++ b/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragment.java
@@ -51,6 +51,7 @@
import com.android.settings.R;
import com.android.settings.SetupWizardUtils;
import com.android.settings.accessibility.AccessibilitySetupWizardUtils;
+import com.android.settings.accessibility.PreferredShortcuts;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -161,6 +162,9 @@
} else if (TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING.equals(uri)) {
refreshPreferenceController(TwoFingersDoubleTapShortcutOptionController.class);
}
+
+ PreferredShortcuts.updatePreferredShortcutsFromSettings(
+ getContext(), mShortcutTargets);
}
};
@@ -212,6 +216,7 @@
final AccessibilityManager am = getSystemService(
AccessibilityManager.class);
am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
+ PreferredShortcuts.updatePreferredShortcutsFromSettings(getContext(), mShortcutTargets);
}
@Override
@@ -270,6 +275,7 @@
}
mShortcutTargets = Set.of(targets);
+ // TODO(318748373): use 'targets' to populate title when no title is given
}
@Override
diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
index 2023299..2f04b62 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -20,6 +20,7 @@
import android.app.Activity;
import android.app.Dialog;
+import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@@ -656,7 +657,11 @@
CombinedProviderInfo.createSettingsActivityIntent(
mContext, packageName, settingsActivity, getUser());
if (settingsIntent != null) {
- mContext.startActivity(settingsIntent);
+ try {
+ mContext.startActivity(settingsIntent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Failed to open settings activity", e);
+ }
}
}
});
diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
index d2400bb..0fb1769 100644
--- a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.applications.credentials;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.credentials.CredentialManager;
@@ -26,6 +27,7 @@
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
import android.view.autofill.AutofillManager;
+import android.util.Slog;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
@@ -132,7 +134,11 @@
new PrimaryProviderPreference.Delegate() {
public void onOpenButtonClicked() {
if (settingsActivityIntent != null) {
- startActivity(settingsActivityIntent);
+ try {
+ startActivity(settingsActivityIntent);
+ } catch (ActivityNotFoundException e) {
+ Slog.e(TAG, "Failed to open settings activity", e);
+ }
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
index 81465ed..44c947d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -19,6 +19,8 @@
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
import android.widget.ImageButton;
import androidx.preference.PreferenceViewHolder;
@@ -30,6 +32,7 @@
public class AudioSharingNamePreference extends ValidatedEditTextPreference {
private static final String TAG = "AudioSharingNamePreference";
+ private boolean mShowQrCodeIcon = false;
public AudioSharingNamePreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -58,17 +61,50 @@
setWidgetLayoutResource(R.layout.preference_widget_qrcode);
}
+ void setShowQrCodeIcon(boolean show) {
+ mShowQrCodeIcon = show;
+ notifyChanged();
+ }
+
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+
+ ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+
+ if (shareButton != null && divider != null) {
+ if (mShowQrCodeIcon) {
+ configureVisibleStateForQrCodeIcon(shareButton, divider);
+ } else {
+ configureInvisibleStateForQrCodeIcon(shareButton, divider);
+ }
+ } else {
+ Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
+ }
+ }
+
+ private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+ divider.setVisibility(View.VISIBLE);
+ shareButton.setVisibility(View.VISIBLE);
shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
- shareButton.setOnClickListener(
- unused ->
- new SubSettingLauncher(getContext())
- .setTitleText("Audio sharing QR code")
- .setDestination(AudioStreamsQrCodeFragment.class.getName())
- .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
- .launch());
+ shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
+ }
+
+ private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+ divider.setVisibility(View.INVISIBLE);
+ shareButton.setVisibility(View.INVISIBLE);
+ shareButton.setOnClickListener(null);
+ }
+
+ private void launchAudioSharingQrCodeFragment() {
+ new SubSettingLauncher(getContext())
+ .setTitleText("Audio sharing QR code")
+ .setDestination(AudioStreamsQrCodeFragment.class.getName())
+ .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
+ .launch();
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
index a3eb188..644e05e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -16,25 +16,128 @@
package com.android.settings.connecteddevice.audiosharing;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
public class AudioSharingNamePreferenceController extends BasePreferenceController
- implements ValidatedEditTextPreference.Validator, Preference.OnPreferenceChangeListener {
+ implements ValidatedEditTextPreference.Validator,
+ Preference.OnPreferenceChangeListener,
+ DefaultLifecycleObserver {
private static final String TAG = "AudioSharingNamePreferenceController";
-
+ private static final boolean DEBUG = BluetoothUtils.D;
private static final String PREF_KEY = "audio_sharing_stream_name";
- private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+ private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastMetadataChanged(
+ int broadcastId, BluetoothLeBroadcastMetadata metadata) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onBroadcastMetadataChanged() broadcastId : "
+ + broadcastId
+ + " metadata: "
+ + metadata);
+ }
+ updateQrCodeIcon(true);
+ }
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onBroadcastStopped() reason : "
+ + reason
+ + " broadcastId: "
+ + broadcastId);
+ }
+ updateQrCodeIcon(false);
+ }
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {
+ Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
+ // Do nothing if update failed.
+ }
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {
+ if (DEBUG) {
+ Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
+ }
+ updateBroadcastName();
+ }
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {}
+ };
+
+ @Nullable private final LocalBluetoothManager mLocalBtManager;
+ @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+ private final Executor mExecutor;
+ private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+ @Nullable private AudioSharingNamePreference mPreference;
public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mLocalBtManager = Utils.getLocalBluetoothManager(context);
+ mBroadcast =
+ (mLocalBtManager != null)
+ ? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
+ : null;
mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ }
}
@Override
@@ -43,16 +146,76 @@
}
@Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreference.setValidator(this);
+ updateBroadcastName();
+ updateQrCodeIcon(isBroadcasting(mLocalBtManager));
+ }
+ }
+
+ @Override
public String getPreferenceKey() {
return PREF_KEY;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
- // TODO: update broadcast when name is changed.
+ if (mPreference != null
+ && mPreference.getSummary() != null
+ && ((String) newValue).contentEquals(mPreference.getSummary())) {
+ return false;
+ }
+
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ if (mBroadcast != null) {
+ mBroadcast.setProgramInfo((String) newValue);
+ if (isBroadcasting(mLocalBtManager)) {
+ // Update broadcast, UI update will be handled after callback
+ mBroadcast.updateBroadcast();
+ } else {
+ // Directly update UI if no ongoing broadcast
+ updateBroadcastName();
+ }
+ }
+ });
return true;
}
+ private void updateBroadcastName() {
+ if (mPreference != null) {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ if (mBroadcast != null) {
+ String name = mBroadcast.getProgramInfo();
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setText(name);
+ mPreference.setSummary(name);
+ }
+ });
+ }
+ });
+ }
+ }
+
+ private void updateQrCodeIcon(boolean show) {
+ if (mPreference != null) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setShowQrCodeIcon(show);
+ }
+ });
+ }
+ }
+
@Override
public boolean isTextValid(String value) {
return mAudioSharingNameTextValidator.isTextValid(value);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
index 9492961..2022eb2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -18,10 +18,27 @@
import com.android.settings.widget.ValidatedEditTextPreference;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
+ * 4 characters and a maximum of 32 human-readable characters.
+ */
public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
+ private static final int MIN_LENGTH = 4;
+ private static final int MAX_LENGTH = 32;
+
@Override
public boolean isTextValid(String value) {
- // TODO: Add validate rule if applicable.
- return true;
+ if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
+ return false;
+ }
+ return isValidUTF8(value);
+ }
+
+ private static boolean isValidUTF8(String value) {
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+ String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+ return value.equals(reconstructedString);
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
new file mode 100644
index 0000000..da0eb2e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 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.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioSharingPasswordPreferenceController extends BasePreferenceController
+ implements ValidatedEditTextPreference.Validator,
+ Preference.OnPreferenceChangeListener,
+ DefaultLifecycleObserver {
+ private static final String PREF_KEY = "audio_sharing_stream_password";
+
+ private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastMetadataChanged(
+ int broadcastId, BluetoothLeBroadcastMetadata metadata) {}
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {}
+ };
+ @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+ private final Executor mExecutor;
+ private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
+ @Nullable private ValidatedEditTextPreference mPreference;
+
+ public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBroadcast =
+ Utils.getLocalBtManager(context).getProfileManager().getLeAudioBroadcastProfile();
+ mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreference.setValidator(this);
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ // TODO(chelseahao): implement
+ return true;
+ }
+
+ @Override
+ public boolean isTextValid(String value) {
+ return mAudioSharingPasswordValidator.isTextValid(value);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
new file mode 100644
index 0000000..dbb40ec
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.connecteddevice.audiosharing;
+
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
+ * and should not exceed 16 octets.
+ */
+public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
+ private static final int MIN_OCTETS = 4;
+ private static final int MAX_OCTETS = 16;
+
+ @Override
+ public boolean isTextValid(String value) {
+ if (value == null
+ || getOctetsCount(value) < MIN_OCTETS
+ || getOctetsCount(value) > MAX_OCTETS) {
+ return false;
+ }
+
+ return isValidUTF8(value);
+ }
+
+ private static int getOctetsCount(String value) {
+ return value.getBytes(StandardCharsets.UTF_8).length;
+ }
+
+ private static boolean isValidUTF8(String value) {
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+ String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+ return value.equals(reconstructedString);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index 924b04d..242ce20 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -336,7 +336,7 @@
}
/** Returns if the broadcast is on-going. */
- public static boolean isBroadcasting(LocalBluetoothManager manager) {
+ public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
if (manager == null) return false;
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
index bb729d6..47597cf 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -16,39 +16,170 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
public class AudioStreamButtonController extends BasePreferenceController
implements DefaultLifecycleObserver {
+ private static final String TAG = "AudioStreamButtonController";
private static final String KEY = "audio_stream_button";
+ private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+ new AudioStreamsBroadcastAssistantCallback() {
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoveFailed(sink, sourceId, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink,
+ int sourceId,
+ BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ if (mAudioStreamsHelper.isConnected(state)) {
+ updateButton();
+ }
+ }
+
+ @Override
+ public void onSourceAddFailed(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+ super.onSourceAddFailed(sink, source, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ updateButton();
+ }
+ };
+
+ private final AudioStreamsRepository mAudioStreamsRepository =
+ AudioStreamsRepository.getInstance();
+ private final Executor mExecutor;
+ private final AudioStreamsHelper mAudioStreamsHelper;
+ private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private @Nullable ActionButtonsPreference mPreference;
private int mBroadcastId = -1;
public AudioStreamButtonController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+ mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
@Override
public final void displayPreference(PreferenceScreen screen) {
mPreference = screen.findPreference(getPreferenceKey());
- if (mPreference != null) {
- mPreference.setButton1Enabled(true);
- // TODO(chelseahao): update this based on stream connection state
- mPreference
- .setButton1Text(R.string.bluetooth_device_context_disconnect)
- .setButton1Icon(R.drawable.ic_settings_close);
- }
+ updateButton();
super.displayPreference(screen);
}
+ private void updateButton() {
+ if (mPreference != null) {
+ if (mAudioStreamsHelper.getAllConnectedSources().stream()
+ .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
+ .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(true);
+ mPreference
+ .setButton1Text(
+ R.string.bluetooth_device_context_disconnect)
+ .setButton1Icon(R.drawable.ic_settings_close)
+ .setButton1OnClickListener(
+ unused -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(false);
+ }
+ mAudioStreamsHelper.removeSource(mBroadcastId);
+ });
+ }
+ });
+ } else {
+ View.OnClickListener clickToRejoin =
+ unused ->
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ var metadata =
+ mAudioStreamsRepository.getSavedMetadata(
+ mContext, mBroadcastId);
+ if (metadata != null) {
+ mAudioStreamsHelper.addSource(metadata);
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(
+ false);
+ }
+ });
+ }
+ });
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(true);
+ mPreference
+ .setButton1Text(R.string.bluetooth_device_context_connect)
+ .setButton1Icon(R.drawable.ic_add_24dp)
+ .setButton1OnClickListener(clickToRejoin);
+ }
+ });
+ }
+ } else {
+ Log.w(TAG, "updateButton(): preference is null!");
+ }
+ }
+
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
index 5981c9e..131c8f6 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
@@ -104,7 +104,7 @@
private Dialog getErrorDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Can't listen to audio stream")
- .setSubTitle1("Can't play this audio stream. Learn more")
+ .setSubTitle2("Can't play this audio stream. Learn more")
.setRightButtonText("Close")
.setRightButtonOnClickListener(
unused -> {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
index 89f24bc..3524543 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -16,22 +16,64 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.LayoutPreference;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
import javax.annotation.Nullable;
public class AudioStreamHeaderController extends BasePreferenceController
implements DefaultLifecycleObserver {
+ private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header";
+ private final Executor mExecutor;
+ private final AudioStreamsHelper mAudioStreamsHelper;
+ @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+ private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+ new AudioStreamsBroadcastAssistantCallback() {
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ updateSummary();
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ updateSummary();
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink,
+ int sourceId,
+ BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ if (mAudioStreamsHelper.isConnected(state)) {
+ updateSummary();
+ }
+ }
+ };
+
private @Nullable EntityHeaderController mHeaderController;
private @Nullable DashboardFragment mFragment;
private String mBroadcastName = "";
@@ -39,6 +81,27 @@
public AudioStreamHeaderController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+ mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
@Override
@@ -55,14 +118,37 @@
}
mHeaderController.setIcon(
screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing));
- // TODO(chelseahao): update this based on stream connection state
- mHeaderController.setSummary("Listening now");
- mHeaderController.done(true);
screen.addPreference(headerPreference);
+ updateSummary();
}
super.displayPreference(screen);
}
+ private void updateSummary() {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ var latestSummary =
+ mAudioStreamsHelper.getAllConnectedSources().stream()
+ .map(
+ BluetoothLeBroadcastReceiveState
+ ::getBroadcastId)
+ .anyMatch(
+ connectedBroadcastId ->
+ connectedBroadcastId
+ == mBroadcastId)
+ ? "Listening now"
+ : "";
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mHeaderController != null) {
+ mHeaderController.setSummary(latestSummary);
+ mHeaderController.done(true);
+ }
+ });
+ });
+ }
+
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 678f952..c2e1178 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -21,8 +21,10 @@
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.AttributeSet;
+import android.view.View;
import androidx.annotation.Nullable;
+import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
@@ -56,7 +58,6 @@
}
mIsConnected = isConnected;
setSummary(summary);
- setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
}
@@ -70,6 +71,23 @@
mAudioStream.setState(state);
}
+ void setAudioStreamMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mAudioStream.setMetadata(metadata);
+ }
+
+ int getAudioStreamBroadcastId() {
+ return mAudioStream.getBroadcastId();
+ }
+
+ int getAudioStreamRssi() {
+ return mAudioStream.getRssi();
+ }
+
+ @Nullable
+ BluetoothLeBroadcastMetadata getAudioStreamMetadata() {
+ return mAudioStream.getMetadata();
+ }
+
AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState();
}
@@ -84,25 +102,31 @@
return R.layout.preference_widget_lock;
}
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+ if (divider != null) {
+ divider.setVisibility(View.GONE);
+ }
+ }
+
static AudioStreamPreference fromMetadata(
- Context context,
- BluetoothLeBroadcastMetadata source,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastMetadata source) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
- preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(source));
return preference;
}
static AudioStreamPreference fromReceiveState(
- Context context,
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastReceiveState receiveState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(receiveState));
- preference.setAudioStream(
- new AudioStream(
- receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(receiveState));
return preference;
}
@@ -127,41 +151,45 @@
}
private static final class AudioStream {
- private int mSourceId;
- private int mBroadcastId;
- private AudioStreamsProgressCategoryController.AudioStreamState mState;
+ private static final int UNAVAILABLE = -1;
+ @Nullable private BluetoothLeBroadcastMetadata mMetadata;
+ @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
+ private AudioStreamsProgressCategoryController.AudioStreamState mState =
+ AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
- private AudioStream(
- int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
}
- private AudioStream(
- int sourceId,
- int broadcastId,
- AudioStreamsProgressCategoryController.AudioStreamState state) {
- mSourceId = sourceId;
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
+ mReceiveState = receiveState;
}
- // TODO(chelseahao): use this to handleSourceRemoved
- private int getSourceId() {
- return mSourceId;
- }
-
- // TODO(chelseahao): use this to handleSourceRemoved
private int getBroadcastId() {
- return mBroadcastId;
+ return mMetadata != null
+ ? mMetadata.getBroadcastId()
+ : mReceiveState != null ? mReceiveState.getBroadcastId() : UNAVAILABLE;
+ }
+
+ private int getRssi() {
+ return mMetadata != null ? mMetadata.getRssi() : Integer.MAX_VALUE;
}
private AudioStreamsProgressCategoryController.AudioStreamState getState() {
return mState;
}
+ @Nullable
+ private BluetoothLeBroadcastMetadata getMetadata() {
+ return mMetadata;
+ }
+
private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mState = state;
}
+
+ private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
+ }
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
index 84e753c..9fb5b21 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -24,21 +24,12 @@
import com.android.settingslib.bluetooth.BluetoothUtils;
-import java.util.Locale;
-
public class AudioStreamsBroadcastAssistantCallback
implements BluetoothLeBroadcastAssistant.Callback {
private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
private static final boolean DEBUG = BluetoothUtils.D;
- private final AudioStreamsProgressCategoryController mCategoryController;
-
- public AudioStreamsBroadcastAssistantCallback(
- AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
- mCategoryController = audioStreamsProgressCategoryController;
- }
-
@Override
public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
@@ -52,45 +43,30 @@
+ " state: "
+ state);
}
- mCategoryController.handleSourceConnected(state);
}
@Override
public void onSearchStartFailed(int reason) {
Log.w(TAG, "onSearchStartFailed() reason : " + reason);
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to start scanning, reason %d", reason));
}
@Override
public void onSearchStarted(int reason) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSearchStarted() reason : " + reason);
}
- mCategoryController.setScanning(true);
}
@Override
public void onSearchStopFailed(int reason) {
Log.w(TAG, "onSearchStopFailed() reason : " + reason);
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
}
@Override
public void onSearchStopped(int reason) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSearchStopped() reason : " + reason);
}
- mCategoryController.setScanning(false);
}
@Override
@@ -106,8 +82,6 @@
+ " reason: "
+ reason);
}
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
}
@Override
@@ -126,14 +100,9 @@
@Override
public void onSourceFound(BluetoothLeBroadcastMetadata source) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSourceFound() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
}
- mCategoryController.handleSourceFound(source);
}
@Override
@@ -141,7 +110,6 @@
if (DEBUG) {
Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
}
- mCategoryController.handleSourceLost(broadcastId);
}
@Override
@@ -153,12 +121,6 @@
@Override
public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
- mCategoryController.showToast(
- String.format(
- Locale.US,
- "Failed to remove source %d for sink %s",
- sourceId,
- sink.getAddress()));
}
@Override
@@ -166,8 +128,5 @@
if (DEBUG) {
Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
}
- mCategoryController.showToast(
- String.format(
- Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
new file mode 100644
index 0000000..34ffc91
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -0,0 +1,117 @@
+/*
+ * 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.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import java.util.Locale;
+
+public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
+ private static final String TAG = "AudioStreamsProgressCategoryCallback";
+
+ private final AudioStreamsProgressCategoryController mCategoryController;
+
+ public AudioStreamsProgressCategoryCallback(
+ AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
+ mCategoryController = audioStreamsProgressCategoryController;
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ mCategoryController.handleSourceConnected(state);
+ }
+
+ @Override
+ public void onSearchStartFailed(int reason) {
+ super.onSearchStartFailed(reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to start scanning, reason %d", reason));
+ }
+
+ @Override
+ public void onSearchStarted(int reason) {
+ super.onSearchStarted(reason);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.setScanning(true);
+ }
+
+ @Override
+ public void onSearchStopFailed(int reason) {
+ super.onSearchStopFailed(reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
+ }
+
+ @Override
+ public void onSearchStopped(int reason) {
+ super.onSearchStopped(reason);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.setScanning(false);
+ }
+
+ @Override
+ public void onSourceAddFailed(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+ super.onSourceAddFailed(sink, source, reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
+ }
+
+ @Override
+ public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+ super.onSourceFound(source);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSourceFound() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.handleSourceFound(source);
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ mCategoryController.handleSourceLost(broadcastId);
+ }
+
+ @Override
+ public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoveFailed(sink, sourceId, reason);
+ mCategoryController.showToast(
+ String.format(
+ Locale.US,
+ "Failed to remove source %d for sink %s",
+ sourceId,
+ sink.getAddress()));
+ }
+
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ mCategoryController.handleSourceRemoved();
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index cb9975d..c6f342a 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -43,8 +45,10 @@
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -53,6 +57,7 @@
import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -74,25 +79,77 @@
}
};
+ private final Preference.OnPreferenceClickListener mAddSourceOrShowDialog =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "preferenceClicked(): attempt to join broadcast id : "
+ + p.getAudioStreamBroadcastId());
+ }
+ var source = p.getAudioStreamMetadata();
+ if (source != null) {
+ if (source.isEncrypted()) {
+ ThreadUtils.postOnMainThread(() -> launchPasswordDialog(source, p));
+ } else {
+ moveToState(p, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
+ }
+ }
+ return true;
+ };
+
+ private final Preference.OnPreferenceClickListener mLaunchDetailFragment =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ Bundle broadcast = new Bundle();
+ broadcast.putString(
+ AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
+ broadcast.putInt(
+ AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
+
+ new SubSettingLauncher(mContext)
+ .setTitleText("Audio stream details")
+ .setDestination(AudioStreamDetailsFragment.class.getName())
+ // TODO(chelseahao): Add logging enum
+ .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+ .setArguments(broadcast)
+ .launch();
+ return true;
+ };
+
+ private final AudioStreamsRepository mAudioStreamsRepository =
+ AudioStreamsRepository.getInstance();
+
enum AudioStreamState {
+ UNKNOWN,
// When mTimedSourceFromQrCode is present and this source has not been synced.
WAIT_FOR_SYNC,
// When source has been synced but not added to any sink.
SYNCED,
// When addSource is called for this source and waiting for response.
- WAIT_FOR_SOURCE_ADD,
+ ADD_SOURCE_WAIT_FOR_RESPONSE,
// Source is added to active sink.
SOURCE_ADDED,
}
+ private final Comparator<AudioStreamPreference> mComparator =
+ Comparator.<AudioStreamPreference, Boolean>comparing(
+ p ->
+ p.getAudioStreamState()
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_ADDED)
+ .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
+ .reversed();
+
private final Executor mExecutor;
- private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
+ private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
- private TimedSourceFromQrCode mTimedSourceFromQrCode;
+ private @Nullable TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
private AudioStreamsDashboardFragment mFragment;
@@ -102,7 +159,7 @@
mBluetoothManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
- mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
+ mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
}
@Override
@@ -155,41 +212,18 @@
}
void handleSourceFound(BluetoothLeBroadcastMetadata source) {
- Preference.OnPreferenceClickListener addSourceOrShowDialog =
- preference -> {
- if (DEBUG) {
- Log.d(
- TAG,
- "preferenceClicked(): attempt to join broadcast id : "
- + source.getBroadcastId());
- }
- if (source.isEncrypted()) {
- ThreadUtils.postOnMainThread(
- () ->
- launchPasswordDialog(
- source, (AudioStreamPreference) preference));
- } else {
- mAudioStreamsHelper.addSource(source);
- ((AudioStreamPreference) preference)
- .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- (AudioStreamPreference) preference,
- AudioStreamState.WAIT_FOR_SOURCE_ADD,
- null);
- }
- return true;
- };
-
var broadcastIdFound = source.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdFound,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- source, AudioStreamState.SYNCED, addSourceOrShowDialog);
+ // No existing preference for this source founded, add one and set initial
+ // state to SYNCED.
+ return addNewPreference(source, AudioStreamState.SYNCED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ if (fromState == AudioStreamState.WAIT_FOR_SYNC
+ && mTimedSourceFromQrCode != null) {
var pendingSource = mTimedSourceFromQrCode.get();
if (pendingSource == null) {
Log.w(
@@ -198,15 +232,20 @@
+ fromState
+ " for broadcastId : "
+ broadcastIdFound);
- v.setAudioStreamState(AudioStreamState.SYNCED);
+ v.setAudioStreamMetadata(source);
+ moveToState(v, AudioStreamState.SYNCED);
return v;
}
- mAudioStreamsHelper.addSource(pendingSource);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source founded is existed from a QR code scan. As the
+ // source is now synced, we update the preference with pendingSource from QR
+ // code scan and add source with it (since it has the password).
+ v.setAudioStreamMetadata(pendingSource);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ // A preference with source founded existed either because it's already
+ // connected (SOURCE_ADDED), or other unexpected reason. We update the
+ // preference with this source and won't change it's state.
+ v.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w(
TAG,
@@ -229,18 +268,18 @@
metadataFromQrCode.getBroadcastId(),
(k, v) -> {
if (v == null) {
- mTimedSourceFromQrCode.waitForConsume();
- return addNewPreference(
- metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
+ // No existing preference for this source from the QR code scan, add one and
+ // set initial state to WAIT_FOR_SYNC.
+ return addNewPreference(metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.SYNCED) {
- mAudioStreamsHelper.addSource(metadataFromQrCode);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source from the QR code is existed because it has been
+ // founded during scanning, now we have the password, we can add source.
+ v.setAudioStreamMetadata(metadataFromQrCode);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ v.setAudioStreamMetadata(metadataFromQrCode);
Log.w(
TAG,
"handleSourceFromQrCode(): unexpected state : "
@@ -265,54 +304,71 @@
mAudioStreamsHelper.removeSource(broadcastId);
}
+ void handleSourceRemoved() {
+ for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
+ var preference = entry.getValue();
+
+ // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
+ // not, means the source is removed from the sink, we move back the preference to SYNCED
+ // state.
+ if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+ && mAudioStreamsHelper.getAllConnectedSources().stream()
+ .noneMatch(
+ connected ->
+ connected.getBroadcastId()
+ == preference.getAudioStreamBroadcastId())) {
+
+ ThreadUtils.postOnMainThread(
+ () -> {
+ var metadata = preference.getAudioStreamMetadata();
+
+ if (metadata != null) {
+ moveToState(preference, AudioStreamState.SYNCED);
+ } else {
+ handleSourceLost(preference.getAudioStreamBroadcastId());
+ }
+ });
+
+ return;
+ }
+ }
+ }
+
void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
- var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- receiveState,
- sourceAddedState,
- p -> launchDetailFragment(broadcastIdConnected));
+ // No existing preference for this source even if it's already connected,
+ // add one and set initial state to SOURCE_ADDED. This could happen because
+ // we retrieves the connected source during onStart() from
+ // AudioStreamsHelper#getAllConnectedSources() even before the source is
+ // founded by scanning.
+ return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
+ if (fromState == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE
|| fromState == AudioStreamState.SYNCED
- || fromState == AudioStreamState.WAIT_FOR_SYNC) {
- if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
- }
+ || fromState == AudioStreamState.WAIT_FOR_SYNC
+ || fromState == AudioStreamState.SOURCE_ADDED) {
+ // Expected state, do nothing
} else {
- if (fromState != AudioStreamState.SOURCE_ADDED) {
- Log.w(
- TAG,
- "handleSourceConnected(): unexpected state : "
- + fromState
- + " for broadcastId : "
- + broadcastIdConnected);
- }
+ Log.w(
+ TAG,
+ "handleSourceConnected(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdConnected);
}
- v.setAudioStreamState(sourceAddedState);
- updatePreferenceConnectionState(
- v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
+ moveToState(v, AudioStreamState.SOURCE_ADDED);
return v;
});
}
- private static String getPreferenceSummary(AudioStreamState state) {
- return switch (state) {
- case WAIT_FOR_SYNC -> "Scanning...";
- case WAIT_FOR_SOURCE_ADD -> "Connecting...";
- case SOURCE_ADDED -> "Listening now";
- default -> "";
- };
- }
-
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -322,7 +378,7 @@
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) {
- mCategoryPreference.removeAll();
+ mCategoryPreference.removeAudioStreamPreferences();
mCategoryPreference.setVisible(hasActive);
}
});
@@ -348,7 +404,6 @@
Log.d(TAG, "startScanning()");
}
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
- mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Handle QR code scan and display currently connected streams
var unused =
@@ -358,6 +413,7 @@
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
+ mLeBroadcastAssistant.startSearchingForSources(emptyList());
});
}
@@ -374,68 +430,93 @@
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
+ mTimedSourceFromQrCode.cleanup();
+ mTimedSourceFromQrCode = null;
}
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
+ moveToState(preference, state);
return preference;
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastMetadata metadata,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
+ moveToState(preference, state);
return preference;
}
- private void updatePreferenceConnectionState(
- AudioStreamPreference preference,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
+ private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+ if (preference.getAudioStreamState() == state) {
+ return;
+ }
+ preference.setAudioStreamState(state);
+
+ // Perform action according to the new state
+ if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ var metadata = preference.getAudioStreamMetadata();
+ if (metadata != null) {
+ mAudioStreamsHelper.addSource(metadata);
+ // Cache the metadata that used for add source, if source is added successfully, we
+ // will save it persistently.
+ mAudioStreamsRepository.cacheMetadata(metadata);
+ }
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ // Saved connected metadata for user to re-join this broadcast later.
+ var cached =
+ mAudioStreamsRepository.getCachedMetadata(
+ preference.getAudioStreamBroadcastId());
+ if (cached != null) {
+ mAudioStreamsRepository.saveMetadata(mContext, cached);
+ }
+ } else if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.waitForConsume();
+ }
+ }
+
+ // Get preference click listener according to the new state
+ Preference.OnPreferenceClickListener listener;
+ if (state == AudioStreamState.SYNCED) {
+ listener = mAddSourceOrShowDialog;
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ listener = mLaunchDetailFragment;
+ } else {
+ listener = null;
+ }
+
+ // Get preference summary according to the new state
+ String summary;
+ if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ summary = "Scanning...";
+ } else if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ summary = "Connecting...";
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ summary = "Listening now";
+ } else {
+ summary = "";
+ }
+
+ // Update UI
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
- state == AudioStreamState.SOURCE_ADDED,
- getPreferenceSummary(state),
- onClickListener);
+ state == AudioStreamState.SOURCE_ADDED, summary, listener);
if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(preference);
+ mCategoryPreference.addAudioStreamPreference(preference, mComparator);
}
});
}
- private boolean launchDetailFragment(int broadcastId) {
- if (!mBroadcastIdToPreferenceMap.containsKey(broadcastId)) {
- Log.w(
- TAG,
- "launchDetailFragment(): broadcastId not exist in BroadcastIdToPreferenceMap!");
- return false;
- }
- AudioStreamPreference preference = mBroadcastIdToPreferenceMap.get(broadcastId);
-
- Bundle broadcast = new Bundle();
- broadcast.putString(
- AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
- broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
-
- new SubSettingLauncher(mContext)
- .setTitleText("Audio stream details")
- .setDestination(AudioStreamDetailsFragment.class.getName())
- // TODO(chelseahao): Add logging enum
- .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
- .setArguments(broadcast)
- .launch();
- return true;
- }
-
private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
@@ -457,15 +538,16 @@
R.id.broadcast_edit_text))
.getText()
.toString();
- mAudioStreamsHelper.addSource(
+ var metadata =
new BluetoothLeBroadcastMetadata.Builder(source)
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
- .build());
- preference.setAudioStreamState(
- AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ .build();
+ // Update the metadata after user entered the password
+ preference.setAudioStreamMetadata(metadata);
+ moveToState(
+ preference,
+ AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
})
.create();
alertDialog.show();
@@ -474,16 +556,17 @@
private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mContext)
.setTitle("Connect compatible headphones")
- .setSubTitle1(
+ .setSubTitle2(
"To listen to an audio stream, first connect headphones that support LE"
+ " Audio to this device. Learn more")
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Connect a device")
.setRightButtonOnClickListener(
- unused ->
- mContext.startActivity(
- new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)));
+ dialog -> {
+ mContext.startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
+ dialog.dismiss();
+ });
}
private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
@@ -495,8 +578,18 @@
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Retry")
- // TODO(chelseahao): Add retry action
- .setRightButtonOnClickListener(AlertDialog::dismiss);
+ .setRightButtonOnClickListener(
+ dialog -> {
+ if (mFragment != null) {
+ Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
+ intent.setAction(
+ BluetoothBroadcastUtils
+ .ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
+ mFragment.startActivityForResult(
+ intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+ dialog.dismiss();
+ }
+ });
}
private class TimedSourceFromQrCode {
@@ -529,11 +622,18 @@
mTimer.start();
}
- private void consumed() {
+ private void cleanup() {
mTimer.cancel();
mSourceFromQrCode = null;
}
+ private void consumed(int broadcastId) {
+ if (mSourceFromQrCode == null || broadcastId != mSourceFromQrCode.getBroadcastId()) {
+ return;
+ }
+ cleanup();
+ }
+
private BluetoothLeBroadcastMetadata get() {
return mSourceFromQrCode;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
index d259900..33adc31 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
@@ -19,9 +19,15 @@
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.NonNull;
+
import com.android.settings.ProgressCategory;
import com.android.settings.R;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
public AudioStreamsProgressCategoryPreference(Context context) {
@@ -46,6 +52,37 @@
init();
}
+ void addAudioStreamPreference(
+ @NonNull AudioStreamPreference preference,
+ Comparator<AudioStreamPreference> comparator) {
+ super.addPreference(preference);
+
+ List<AudioStreamPreference> preferences = getAllAudioStreamPreferences();
+ preferences.sort(comparator);
+ for (int i = 0; i < preferences.size(); i++) {
+ // setOrder to i + 1, since the order 0 preference should always be the
+ // "audio_streams_scan_qr_code"
+ preferences.get(i).setOrder(i + 1);
+ }
+ }
+
+ void removeAudioStreamPreferences() {
+ List<AudioStreamPreference> streams = getAllAudioStreamPreferences();
+ for (var toRemove : streams) {
+ removePreference(toRemove);
+ }
+ }
+
+ private List<AudioStreamPreference> getAllAudioStreamPreferences() {
+ List<AudioStreamPreference> streams = new ArrayList<>();
+ for (int i = 0; i < getPreferenceCount(); i++) {
+ if (getPreference(i) instanceof AudioStreamPreference) {
+ streams.add((AudioStreamPreference) getPreference(i));
+ }
+ }
+ return streams;
+ }
+
private void init() {
setEmptyTextRes(R.string.audio_streams_empty);
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
index 42b38ee..2366e70 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
@@ -24,6 +24,9 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
@@ -34,6 +37,7 @@
import com.google.zxing.WriterException;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
@@ -49,30 +53,47 @@
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
- getQrCodeBitmap()
- .ifPresent(
- bm ->
+
+ BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+
+ if (broadcastMetadata != null) {
+ getQrCodeBitmap(broadcastMetadata)
+ .ifPresent(
+ bm -> {
((ImageView) view.requireViewById(R.id.qrcode_view))
- .setImageBitmap(bm));
+ .setImageBitmap(bm);
+ ((TextView) view.requireViewById(R.id.password))
+ .setText(
+ "Password: "
+ + new String(
+ broadcastMetadata
+ .getBroadcastCode(),
+ StandardCharsets.UTF_8));
+ });
+ }
return view;
}
- private Optional<Bitmap> getQrCodeBitmap() {
- String broadcastMetadata = getBroadcastMetadataQrCode();
- if (broadcastMetadata.isEmpty()) {
+ private Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
+ if (metadata == null) {
Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
return Optional.empty();
}
-
+ String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ if (metadataStr.isEmpty()) {
+ Log.d(TAG, "onCreateView: metadataStr is empty!");
+ return Optional.empty();
+ }
+ Log.d("chelsea", metadataStr);
try {
int qrcodeSize = getContext().getResources().getDimensionPixelSize(R.dimen.qrcode_size);
- Bitmap bitmap = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize);
+ Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
return Optional.of(bitmap);
} catch (WriterException e) {
Log.d(
TAG,
"onCreateView: broadcastMetadata "
- + broadcastMetadata
+ + metadata
+ " qrCode generation exception "
+ e);
}
@@ -80,23 +101,24 @@
return Optional.empty();
}
- private String getBroadcastMetadataQrCode() {
+ @Nullable
+ private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
Utils.getLocalBtManager(getActivity())
.getProfileManager()
.getLeAudioBroadcastProfile();
if (localBluetoothLeBroadcast == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
- return "";
+ return null;
}
BluetoothLeBroadcastMetadata metadata =
localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
- return "";
+ return null;
}
- return BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ return metadata;
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
new file mode 100644
index 0000000..65245ac
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
@@ -0,0 +1,160 @@
+/*
+ * 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.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
+
+/** Manages the caching and storage of Bluetooth audio stream metadata. */
+public class AudioStreamsRepository {
+
+ private static final String TAG = "AudioStreamsRepository";
+ private static final boolean DEBUG = BluetoothUtils.D;
+
+ private static final String PREF_KEY = "bluetooth_audio_stream_pref";
+ private static final String METADATA_KEY = "bluetooth_audio_stream_metadata";
+
+ @Nullable
+ private static AudioStreamsRepository sInstance = null;
+
+ private AudioStreamsRepository() {}
+
+ /**
+ * Gets the single instance of AudioStreamsRepository.
+ *
+ * @return The AudioStreamsRepository instance.
+ */
+ public static synchronized AudioStreamsRepository getInstance() {
+ if (sInstance == null) {
+ sInstance = new AudioStreamsRepository();
+ }
+ return sInstance;
+ }
+
+ private final ConcurrentHashMap<Integer, BluetoothLeBroadcastMetadata>
+ mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>();
+
+ /**
+ * Caches BluetoothLeBroadcastMetadata in a local cache.
+ *
+ * @param metadata The BluetoothLeBroadcastMetadata to be cached.
+ */
+ void cacheMetadata(BluetoothLeBroadcastMetadata metadata) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "cacheMetadata(): broadcastId "
+ + metadata.getBroadcastId()
+ + " saved in local cache.");
+ }
+ mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata);
+ }
+
+ /**
+ * Gets cached BluetoothLeBroadcastMetadata by broadcastId.
+ *
+ * @param broadcastId The broadcastId to look up in the cache.
+ * @return The cached BluetoothLeBroadcastMetadata or null if not found.
+ */
+ @Nullable
+ BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) {
+ var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId);
+ if (metadata == null) {
+ Log.w(
+ TAG,
+ "getCachedMetadata(): broadcastId not found in"
+ + " mBroadcastIdToMetadataCacheMap.");
+ return null;
+ }
+ return metadata;
+ }
+
+ /**
+ * Saves metadata to SharedPreferences asynchronously.
+ *
+ * @param context The context.
+ * @param metadata The BluetoothLeBroadcastMetadata to be saved.
+ */
+ void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+ if (sharedPref != null) {
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(
+ METADATA_KEY,
+ BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(
+ metadata));
+ editor.apply();
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "saveMetadata(): broadcastId "
+ + metadata.getBroadcastId()
+ + " metadata saved in storage.");
+ }
+ }
+ });
+ }
+
+ /**
+ * Gets saved metadata from SharedPreferences.
+ *
+ * @param context The context.
+ * @param broadcastId The broadcastId to retrieve metadata for.
+ * @return The saved BluetoothLeBroadcastMetadata or null if not found.
+ */
+ @Nullable
+ BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) {
+ SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+ if (sharedPref != null) {
+ String savedMetadataStr = sharedPref.getString(METADATA_KEY, null);
+ if (savedMetadataStr == null) {
+ Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null");
+ return null;
+ }
+ var savedMetadata =
+ BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+ savedMetadataStr);
+ if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) {
+ Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id.");
+ return null;
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "getSavedMetadata(): broadcastId "
+ + savedMetadata.getBroadcastId()
+ + " metadata found in storage.");
+ }
+ return savedMetadata;
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 549e725..24e1ca3 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -16,7 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
-import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
@@ -58,14 +57,12 @@
};
private final LocalBluetoothManager mLocalBtManager;
- private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment;
private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext);
- mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
}
public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -124,10 +121,6 @@
});
}
- void addSource(BluetoothLeBroadcastMetadata source) {
- mAudioStreamsHelper.addSource(source);
- }
-
private void updateVisibility() {
ThreadUtils.postOnBackgroundThread(
() -> {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
index 2b52039..378128d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
@@ -229,6 +229,7 @@
}
mErrorMessage.setVisibility(View.INVISIBLE);
+ mTextureView.setVisibility(View.INVISIBLE);
triggerVibrationForQrCodeRecognition(getContext());
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 3950031..8fee052 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -34,6 +34,7 @@
import com.android.settings.accessibility.ToggleColorInversionPreferenceFragment;
import com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment;
import com.android.settings.accessibility.ToggleReduceBrightColorsPreferenceFragment;
+import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment;
import com.android.settings.accounts.AccountDashboardFragment;
import com.android.settings.accounts.AccountSyncSettings;
import com.android.settings.accounts.ChooseAccountFragment;
@@ -250,6 +251,7 @@
AccessibilityDetailsSettingsFragment.class.getName(),
AccessibilitySettings.class.getName(),
AccessibilitySettingsForSetupWizard.class.getName(),
+ EditShortcutsPreferenceFragment.class.getName(),
TextReadingPreferenceFragment.class.getName(),
TextReadingPreferenceFragmentForSetupWizard.class.getName(),
CaptioningPropertiesFragment.class.getName(),
diff --git a/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java
new file mode 100644
index 0000000..06cfe65
--- /dev/null
+++ b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 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.development;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.sysprop.BluetoothProperties;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+
+/**
+ * Preference controller to control Bluetooth LE audio mode
+ */
+public class BluetoothLeAudioModePreferenceController
+ extends DeveloperOptionsPreferenceController
+ implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
+
+ private static final String PREFERENCE_KEY = "bluetooth_leaudio_mode";
+
+ static final String LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY =
+ "persist.bluetooth.leaudio_dynamic_switcher.mode";
+
+ @Nullable private final DevelopmentSettingsDashboardFragment mFragment;
+
+ private final String[] mListValues;
+ private final String[] mListSummaries;
+ @VisibleForTesting
+ @Nullable String mNewMode;
+ @VisibleForTesting
+ BluetoothAdapter mBluetoothAdapter;
+
+ boolean mChanged = false;
+
+ public BluetoothLeAudioModePreferenceController(@NonNull Context context,
+ @Nullable DevelopmentSettingsDashboardFragment fragment) {
+ super(context);
+ mFragment = fragment;
+ mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
+
+ mListValues = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode_values);
+ mListSummaries = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode);
+ }
+
+ @Override
+ @NonNull public String getPreferenceKey() {
+ return PREFERENCE_KEY;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false);
+ }
+
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ if (mFragment == null) {
+ return false;
+ }
+
+ BluetoothRebootDialog.show(mFragment);
+ mChanged = true;
+ mNewMode = newValue.toString();
+ return false;
+ }
+
+ @Override
+ public void updateState(@NonNull Preference preference) {
+ if (mBluetoothAdapter == null) {
+ return;
+ }
+
+ if (mBluetoothAdapter.isLeAudioBroadcastSourceSupported()
+ == BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "broadcast");
+ } else if (mBluetoothAdapter.isLeAudioSupported()
+ == BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "unicast");
+ } else {
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "disabled");
+ }
+
+ final String currentValue = SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY);
+ int index = 0;
+ for (int i = 0; i < mListValues.length; i++) {
+ if (TextUtils.equals(currentValue, mListValues[i])) {
+ index = i;
+ break;
+ }
+ }
+
+ final ListPreference listPreference = (ListPreference) preference;
+ listPreference.setValue(mListValues[index]);
+ listPreference.setSummary(mListSummaries[index]);
+ }
+
+ /**
+ * Called when the RebootDialog confirm is clicked.
+ */
+ public void onRebootDialogConfirmed() {
+ if (!mChanged) {
+ return;
+ }
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mNewMode);
+ }
+
+ /**
+ * Called when the RebootDialog cancel is clicked.
+ */
+ public void onRebootDialogCanceled() {
+ mChanged = false;
+ }
+}
diff --git a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
index f1b81b4..2a544f2 100644
--- a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
+++ b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
@@ -21,6 +21,7 @@
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.SystemProperties;
+import android.sysprop.BluetoothProperties;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -65,6 +66,12 @@
}
@Override
+ public boolean isAvailable() {
+ return BluetoothProperties.isProfileBapUnicastClientEnabled().orElse(false)
+ && !BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false);
+ }
+
+ @Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
BluetoothRebootDialog.show(mFragment);
mChanged = true;
diff --git a/src/com/android/settings/development/ClearAdbKeysPreferenceController.java b/src/com/android/settings/development/ClearAdbKeysPreferenceController.java
index b39d874..69e6c69 100644
--- a/src/com/android/settings/development/ClearAdbKeysPreferenceController.java
+++ b/src/com/android/settings/development/ClearAdbKeysPreferenceController.java
@@ -52,6 +52,9 @@
@Override
public boolean isAvailable() {
+ // If the build is insecure (any -user build, 'ro.adb.secure=0'), adbd does not
+ // requests/store authorizations. There is no need for a "revoke authorizations"
+ // button.
return AdbProperties.secure().orElse(false);
}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 73567bc..504eda8 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -454,6 +454,11 @@
getDevelopmentOptionsController(
BluetoothLeAudioPreferenceController.class);
leAudioFeatureController.onRebootDialogConfirmed();
+
+ final BluetoothLeAudioModePreferenceController leAudioModeController =
+ getDevelopmentOptionsController(
+ BluetoothLeAudioModePreferenceController.class);
+ leAudioModeController.onRebootDialogConfirmed();
}
@Override
@@ -471,6 +476,11 @@
getDevelopmentOptionsController(
BluetoothLeAudioPreferenceController.class);
leAudioFeatureController.onRebootDialogCanceled();
+
+ final BluetoothLeAudioModePreferenceController leAudioModeController =
+ getDevelopmentOptionsController(
+ BluetoothLeAudioModePreferenceController.class);
+ leAudioModeController.onRebootDialogCanceled();
}
@Override
@@ -670,6 +680,7 @@
controllers.add(new BluetoothAvrcpVersionPreferenceController(context));
controllers.add(new BluetoothMapVersionPreferenceController(context));
controllers.add(new BluetoothLeAudioPreferenceController(context, fragment));
+ controllers.add(new BluetoothLeAudioModePreferenceController(context, fragment));
controllers.add(new BluetoothLeAudioDeviceDetailsPreferenceController(context));
controllers.add(new BluetoothLeAudioAllowListPreferenceController(context, fragment));
controllers.add(new BluetoothA2dpHwOffloadPreferenceController(context, fragment));
diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
index c40212b..c6b1bdb 100644
--- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
+++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
@@ -41,6 +41,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
+import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.settings.R;
import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial;
import com.android.settings.core.SubSettingLauncher;
@@ -353,7 +354,7 @@
private boolean isAnyServiceSupportAccessibilityButton() {
final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class);
final List<String> targets = ams.getAccessibilityShortcutTargets(
- AccessibilityManager.ACCESSIBILITY_BUTTON);
+ ShortcutConstants.UserShortcutType.SOFTWARE);
return !targets.isEmpty();
}
diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
index 7de505e..b06edb2 100644
--- a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
+++ b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java
@@ -70,6 +70,7 @@
private static final String KEYBOARD_A11Y_CATEGORY = "keyboard_a11y_category";
private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
private static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys";
+ private static final String ACCESSIBILITY_SLOW_KEYS = "accessibility_slow_keys";
private static final String ACCESSIBILITY_STICKY_KEYS = "accessibility_sticky_keys";
private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings";
@@ -78,6 +79,8 @@
Secure.SHOW_IME_WITH_HARD_KEYBOARD);
private static final Uri sAccessibilityBounceKeysUri = Secure.getUriFor(
Secure.ACCESSIBILITY_BOUNCE_KEYS);
+ private static final Uri sAccessibilitySlowKeysUri = Secure.getUriFor(
+ Secure.ACCESSIBILITY_SLOW_KEYS);
private static final Uri sAccessibilityStickyKeysUri = Secure.getUriFor(
Secure.ACCESSIBILITY_STICKY_KEYS);
@@ -97,6 +100,8 @@
@Nullable
private TwoStatePreference mAccessibilityBounceKeys = null;
@Nullable
+ private TwoStatePreference mAccessibilitySlowKeys = null;
+ @Nullable
private TwoStatePreference mAccessibilityStickyKeys = null;
@@ -127,6 +132,8 @@
mKeyboardA11yCategory = Objects.requireNonNull(findPreference(KEYBOARD_A11Y_CATEGORY));
mAccessibilityBounceKeys = Objects.requireNonNull(
mKeyboardA11yCategory.findPreference(ACCESSIBILITY_BOUNCE_KEYS));
+ mAccessibilitySlowKeys = Objects.requireNonNull(
+ mKeyboardA11yCategory.findPreference(ACCESSIBILITY_SLOW_KEYS));
mAccessibilityStickyKeys = Objects.requireNonNull(
mKeyboardA11yCategory.findPreference(ACCESSIBILITY_STICKY_KEYS));
@@ -147,6 +154,9 @@
if (!InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
mKeyboardA11yCategory.removePreference(mAccessibilityBounceKeys);
}
+ if (!InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ mKeyboardA11yCategory.removePreference(mAccessibilitySlowKeys);
+ }
if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
mKeyboardA11yCategory.removePreference(mAccessibilityStickyKeys);
}
@@ -196,6 +206,8 @@
mShowVirtualKeyboardSwitchPreferenceChangeListener);
Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener(
mAccessibilityBounceKeysSwitchPreferenceChangeListener);
+ Objects.requireNonNull(mAccessibilitySlowKeys).setOnPreferenceChangeListener(
+ mAccessibilitySlowKeysSwitchPreferenceChangeListener);
Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener(
mAccessibilityStickyKeysSwitchPreferenceChangeListener);
registerSettingsObserver();
@@ -208,6 +220,7 @@
mIm.unregisterInputDeviceListener(this);
Objects.requireNonNull(mShowVirtualKeyboardSwitch).setOnPreferenceChangeListener(null);
Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener(null);
+ Objects.requireNonNull(mAccessibilitySlowKeys).setOnPreferenceChangeListener(null);
Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener(null);
unregisterSettingsObserver();
}
@@ -315,10 +328,12 @@
updateShowVirtualKeyboardSwitch();
if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()
- || InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ || InputSettings.isAccessibilityStickyKeysFeatureEnabled()
+ || InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
Objects.requireNonNull(mKeyboardA11yCategory).setOrder(2);
preferenceScreen.addPreference(mKeyboardA11yCategory);
updateAccessibilityBounceKeysSwitch();
+ updateAccessibilitySlowKeysSwitch();
updateAccessibilityStickyKeysSwitch();
}
}
@@ -356,6 +371,13 @@
mContentObserver,
UserHandle.myUserId());
}
+ if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ contentResolver.registerContentObserver(
+ sAccessibilitySlowKeysUri,
+ false,
+ mContentObserver,
+ UserHandle.myUserId());
+ }
if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
contentResolver.registerContentObserver(
sAccessibilityStickyKeysUri,
@@ -365,6 +387,7 @@
}
updateShowVirtualKeyboardSwitch();
updateAccessibilityBounceKeysSwitch();
+ updateAccessibilitySlowKeysSwitch();
updateAccessibilityStickyKeysSwitch();
}
@@ -385,6 +408,14 @@
InputSettings.isAccessibilityBounceKeysEnabled(getContext()));
}
+ private void updateAccessibilitySlowKeysSwitch() {
+ if (!InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ return;
+ }
+ Objects.requireNonNull(mAccessibilitySlowKeys).setChecked(
+ InputSettings.isAccessibilitySlowKeysEnabled(getContext()));
+ }
+
private void updateAccessibilityStickyKeysSwitch() {
if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
return;
@@ -414,6 +445,13 @@
};
private final OnPreferenceChangeListener
+ mAccessibilitySlowKeysSwitchPreferenceChangeListener = (preference, newValue) -> {
+ InputSettings.setAccessibilitySlowKeysThreshold(getContext(),
+ ((Boolean) newValue) ? 500 : 0);
+ return true;
+ };
+
+ private final OnPreferenceChangeListener
mAccessibilityStickyKeysSwitchPreferenceChangeListener = (preference, newValue) -> {
InputSettings.setAccessibilityStickyKeysEnabled(getContext(), (Boolean) newValue);
return true;
@@ -426,6 +464,8 @@
updateShowVirtualKeyboardSwitch();
} else if (sAccessibilityBounceKeysUri.equals(uri)) {
updateAccessibilityBounceKeysSwitch();
+ } else if (sAccessibilitySlowKeysUri.equals(uri)) {
+ updateAccessibilitySlowKeysSwitch();
} else if (sAccessibilityStickyKeysUri.equals(uri)) {
updateAccessibilityStickyKeysSwitch();
}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt
index 09b1150..e722866 100644
--- a/src/com/android/settings/network/MobileNetworkListFragment.kt
+++ b/src/com/android/settings/network/MobileNetworkListFragment.kt
@@ -26,8 +26,11 @@
import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.flags.Flags
import com.android.settings.network.telephony.MobileNetworkUtils
import com.android.settings.search.BaseSearchIndexProvider
+import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
+import com.android.settings.spa.network.NetworkCellularGroupProvider
import com.android.settingslib.search.SearchIndexable
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager
@@ -40,6 +43,15 @@
collectAirplaneModeAndFinishIfOn()
}
+ override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+
+ if (Flags.isDualSimOnboardingEnabled()) {
+ context?.startSpaActivity(NetworkCellularGroupProvider.name);
+ finish()
+ }
+ }
+
override fun onResume() {
super.onResume()
// Disable the animation of the preference list
diff --git a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
index ed930d4..f682002 100644
--- a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
+++ b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
@@ -32,7 +32,19 @@
application.getSystemService(SubscriptionManager::class.java)!!
private val scope = viewModelScope + Dispatchers.Default
+ /**
+ * Getting the active Subscription list
+ */
+ //ToDo: renaming the function name
val subscriptionInfoListFlow = application.subscriptionsChangedFlow().map {
SubscriptionUtil.getActiveSubscriptions(subscriptionManager)
}.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
+
+ /**
+ * Getting the Selectable SubscriptionInfo List from the SubscriptionManager's
+ * getAvailableSubscriptionInfoList
+ */
+ val selectableSubscriptionInfoListFlow = application.subscriptionsChangedFlow().map {
+ SubscriptionUtil.getSelectableSubscriptionInfoList(application)
+ }.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
}
diff --git a/src/com/android/settings/network/telephony/NetworkSelectSettings.java b/src/com/android/settings/network/telephony/NetworkSelectSettings.java
index 9f0e605..461930b 100644
--- a/src/com/android/settings/network/telephony/NetworkSelectSettings.java
+++ b/src/com/android/settings/network/telephony/NetworkSelectSettings.java
@@ -420,7 +420,7 @@
cellular network. Therefore, it is needed to filter out satellite plmns from current cell
info list */
private List<CellInfo> filterOutSatellitePlmn(List<CellInfo> cellInfoList) {
- List<String> aggregatedSatellitePlmn = getAllSatellitePlmnsForCarrierWrapper();
+ List<String> aggregatedSatellitePlmn = getSatellitePlmnsForCarrierWrapper();
if (!mShouldFilterOutSatellitePlmn.get() || aggregatedSatellitePlmn.isEmpty()) {
return cellInfoList;
}
@@ -431,13 +431,13 @@
}
/**
- * Serves as a wrapper method for {@link SatelliteManager#getAllSatellitePlmnsForCarrier(int)}.
+ * Serves as a wrapper method for {@link SatelliteManager#getSatellitePlmnsForCarrier(int)}.
* Since SatelliteManager is final, this wrapper enables mocking or spying of
- * {@link SatelliteManager#getAllSatellitePlmnsForCarrier(int)} for unit testing purposes.
+ * {@link SatelliteManager#getSatellitePlmnsForCarrier(int)} for unit testing purposes.
*/
@VisibleForTesting
- protected List<String> getAllSatellitePlmnsForCarrierWrapper() {
- return mSatelliteManager.getAllSatellitePlmnsForCarrier(mSubId);
+ protected List<String> getSatellitePlmnsForCarrierWrapper() {
+ return mSatelliteManager.getSatellitePlmnsForCarrier(mSubId);
}
private void handleCarrierConfigChanged(int subId) {
diff --git a/src/com/android/settings/network/telephony/SatelliteSetting.java b/src/com/android/settings/network/telephony/SatelliteSetting.java
index ecfa8e4..b6d018a 100644
--- a/src/com/android/settings/network/telephony/SatelliteSetting.java
+++ b/src/com/android/settings/network/telephony/SatelliteSetting.java
@@ -183,7 +183,7 @@
private boolean isSatelliteEligible() {
try {
Set<Integer> restrictionReason =
- mSatelliteManager.getSatelliteAttachRestrictionReasonsForCarrier(mSubId);
+ mSatelliteManager.getAttachRestrictionReasonsForCarrier(mSubId);
return !restrictionReason.contains(
SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT);
} catch (SecurityException | IllegalStateException | IllegalArgumentException ex) {
diff --git a/src/com/android/settings/network/telephony/SatelliteSettingPreferenceController.java b/src/com/android/settings/network/telephony/SatelliteSettingPreferenceController.java
index 7de7fcb..94940b3 100644
--- a/src/com/android/settings/network/telephony/SatelliteSettingPreferenceController.java
+++ b/src/com/android/settings/network/telephony/SatelliteSettingPreferenceController.java
@@ -113,7 +113,7 @@
private void updateSummary(Preference preference) {
try {
Set<Integer> restrictionReason =
- mSatelliteManager.getSatelliteAttachRestrictionReasonsForCarrier(mSubId);
+ mSatelliteManager.getAttachRestrictionReasonsForCarrier(mSubId);
boolean isSatelliteEligible = !restrictionReason.contains(
SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT);
if (mIsSatelliteEligible == null || mIsSatelliteEligible != isSatelliteEligible) {
diff --git a/src/com/android/settings/security/ScreenPinningSettings.java b/src/com/android/settings/security/ScreenPinningSettings.java
index 99d6492..8690847 100644
--- a/src/com/android/settings/security/ScreenPinningSettings.java
+++ b/src/com/android/settings/security/ScreenPinningSettings.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.res.Resources;
import android.icu.text.MessageFormat;
import android.os.Bundle;
import android.os.UserHandle;
@@ -30,6 +31,7 @@
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
@@ -45,7 +47,11 @@
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settingslib.search.SearchIndexable;
+import com.android.settingslib.search.SearchIndexableRaw;
import com.android.settingslib.widget.FooterPreference;
+
+import java.util.List;
+
/**
* Screen pinning settings.
*/
@@ -174,9 +180,8 @@
}
}
- private int getCurrentSecurityTitle() {
- int quality = mLockPatternUtils.getKeyguardStoredPasswordQuality(
- UserHandle.myUserId());
+ private static int getCurrentSecurityTitle(LockPatternUtils lockPatternUtils) {
+ int quality = lockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId());
switch (quality) {
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
@@ -187,7 +192,7 @@
case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
return R.string.screen_pinning_unlock_password;
case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
- if (mLockPatternUtils.isLockPatternEnabled(UserHandle.myUserId())) {
+ if (lockPatternUtils.isLockPatternEnabled(UserHandle.myUserId())) {
return R.string.screen_pinning_unlock_pattern;
}
}
@@ -232,7 +237,7 @@
}
});
mUseScreenLock.setChecked(isScreenLockUsed());
- mUseScreenLock.setTitle(getCurrentSecurityTitle());
+ mUseScreenLock.setTitle(getCurrentSecurityTitle(mLockPatternUtils));
} else {
mFooterPreference.setSummary(getAppPinningContent());
mUseScreenLock.setEnabled(false);
@@ -252,8 +257,30 @@
}
/**
- * For search
+ * For search.
+ *
+ * This page only provides an index for the toggle preference of using screen lock for
+ * unpinning. The preference name will change with various lock configurations. Indexing data
+ * from XML isn't suitable since it uses a static title by default. So, we skip XML indexing
+ * by omitting the XML argument in the constructor and use a dynamic index method instead.
*/
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
- new BaseSearchIndexProvider(R.xml.screen_pinning_settings);
+ new BaseSearchIndexProvider() {
+
+ @NonNull
+ @Override
+ public List<SearchIndexableRaw> getDynamicRawDataToIndex(@NonNull Context context,
+ boolean enabled) {
+ List<SearchIndexableRaw> dynamicRaws =
+ super.getDynamicRawDataToIndex(context, enabled);
+ final SearchIndexableRaw raw = new SearchIndexableRaw(context);
+ final Resources res = context.getResources();
+ final LockPatternUtils lockPatternUtils = new LockPatternUtils(context);
+ raw.key = KEY_USE_SCREEN_LOCK;
+ raw.title = res.getString(getCurrentSecurityTitle(lockPatternUtils));
+ raw.screenTitle = res.getString(R.string.screen_pinning_title);
+ dynamicRaws.add(raw);
+ return dynamicRaws;
+ }
+ };
}
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index ac1af80..41852e5 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -48,6 +48,7 @@
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
import com.android.settings.spa.home.HomePageProvider
import com.android.settings.spa.network.NetworkAndInternetPageProvider
+import com.android.settings.spa.network.NetworkCellularGroupProvider
import com.android.settings.spa.network.SimOnboardingPageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.notification.NotificationMainPageProvider
@@ -118,6 +119,7 @@
ApnEditPageProvider,
SimOnboardingPageProvider,
BatteryOptimizationModeAppListPageProvider,
+ NetworkCellularGroupProvider,
)
override val logger = if (FeatureFlagUtils.isEnabled(
diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
index aafe493..1225806 100644
--- a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
@@ -20,24 +20,12 @@
import android.app.AppOpsManager
import android.app.settings.SettingsEnums
import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalContext
import com.android.settings.R
import com.android.settings.overlay.FeatureFactory
-import com.android.settingslib.spa.widget.preference.SwitchPreference
-import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spaprivileged.model.app.AppOpsController
import com.android.settingslib.spaprivileged.model.app.PackageManagers.hasGrantPermission
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
-import kotlinx.coroutines.Dispatchers
/**
* This class builds an App List under voice activation apps and the individual page which
@@ -56,97 +44,15 @@
override val appOp = AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
override val permission = Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO
override val setModeByUid = true
- private var receiveDetectionTrainingDataOpController:AppOpsController? = null
+
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
super.setAllowed(record, newAllowed)
- if (!newAllowed && receiveDetectionTrainingDataOpController != null) {
- receiveDetectionTrainingDataOpController!!.setAllowed(false)
- }
logPermissionChange(newAllowed)
}
override fun isChangeable(record: AppOpPermissionRecord): Boolean =
super.isChangeable(record) && record.app.hasGrantPermission(permission)
- @Composable
- override fun InfoPageAdditionalContent(
- record: AppOpPermissionRecord,
- isAllowed: () -> Boolean?,
- ) {
- SwitchPreference(createReceiveDetectionTrainingDataOpSwitchModel(record, isAllowed))
- }
-
- @Composable
- private fun createReceiveDetectionTrainingDataOpSwitchModel(
- record: AppOpPermissionRecord,
- isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean?
- ): ReceiveDetectionTrainingDataOpSwitchModel {
- val context = LocalContext.current
- receiveDetectionTrainingDataOpController = remember {
- AppOpsController(
- context = context,
- app = record.app,
- op = AppOpsManager.OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
- )
- }
- val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, receiveDetectionTrainingDataOpController!!)
-
- return remember(record) {
- ReceiveDetectionTrainingDataOpSwitchModel(
- context,
- record,
- isReceiveSandBoxTriggerAudioOpAllowed,
- receiveDetectionTrainingDataOpController!!,
- isReceiveDetectionTrainingDataOpAllowed,
- )
- }.also { model -> LaunchedEffect(model, Dispatchers.Default) { model.initState() } }
- }
-
- private inner class ReceiveDetectionTrainingDataOpSwitchModel(
- context: Context,
- private val record: AppOpPermissionRecord,
- isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean?,
- receiveDetectionTrainingDataOpController: AppOpsController,
- isReceiveDetectionTrainingDataOpAllowed: () -> Boolean?,
- ) : SwitchPreferenceModel {
- private var appChangeable by mutableStateOf(true)
-
- override val title: String = context.getString(R.string.permit_receive_sandboxed_detection_training_data)
- override val summary: () -> String = { context.getString(R.string.receive_sandboxed_detection_training_data_description) }
- override val checked = { isReceiveDetectionTrainingDataOpAllowed() == true && isReceiveSandBoxTriggerAudioOpAllowed() == true }
- override val changeable = { appChangeable && isReceiveSandBoxTriggerAudioOpAllowed() == true }
-
- fun initState() {
- appChangeable = isChangeable(record)
- }
-
- override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
- receiveDetectionTrainingDataOpController.setAllowed(newChecked)
- }
- }
-
- @Composable
- private fun isReceiveDetectionTrainingDataOpAllowed(
- record: AppOpPermissionRecord,
- controller: AppOpsController
- ): () -> Boolean? {
- if (record.hasRequestBroaderPermission) {
- // Broader permission trumps the specific permission.
- return { true }
- }
-
- val mode = controller.mode.observeAsState()
- return {
- when (mode.value) {
- null -> null
- AppOpsManager.MODE_ALLOWED -> true
- AppOpsManager.MODE_DEFAULT -> record.app.hasGrantPermission(
- Manifest.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA)
- else -> false
- }
- }
- }
-
private fun logPermissionChange(newAllowed: Boolean) {
val category = when {
newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_RECEIVE_SANDBOX_TRIGGER_AUDIO_ALLOW
diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
new file mode 100644
index 0000000..e746d4a
--- /dev/null
+++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
@@ -0,0 +1,465 @@
+/*
+ * 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.spa.network
+
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Bundle
+import android.os.UserManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import android.util.Log
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Message
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.material.icons.outlined.DataUsage
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import com.android.settings.R
+import com.android.settings.network.SubscriptionInfoListViewModel
+import com.android.settings.network.SubscriptionUtil
+import com.android.settings.network.telephony.MobileNetworkUtils
+import com.android.settings.wifi.WifiPickerTrackerHelper
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
+import com.android.settingslib.spa.widget.preference.ListPreferenceOption
+import com.android.settingslib.spa.widget.preference.Preference
+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 com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
+import com.android.settingslib.spa.widget.ui.SettingsIcon
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
+
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Showing the sim onboarding which is the process flow of sim switching on.
+ */
+object NetworkCellularGroupProvider : SettingsPageProvider {
+ override val name = "NetworkCellularGroupProvider"
+
+ private lateinit var subscriptionViewModel: SubscriptionInfoListViewModel
+ private val owner = createSettingsPage()
+
+ var selectableSubscriptionInfoList: List<SubscriptionInfo> = listOf()
+ var defaultVoiceSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ var defaultSmsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ var defaultDataSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ var nonDds: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+
+ fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
+ .setUiLayoutFn {
+ // never using
+ Preference(object : PreferenceModel {
+ override val title = name
+ override val onClick = navigator(name)
+ })
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ val context = LocalContext.current
+ var selectableSubscriptionInfoListRemember = remember {
+ mutableListOf<SubscriptionInfo>().toMutableStateList()
+ }
+ var callsSelectedId = rememberSaveable {
+ mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ }
+ var textsSelectedId = rememberSaveable {
+ mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ }
+ var mobileDataSelectedId = rememberSaveable {
+ mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ }
+ var nonDdsRemember = rememberSaveable {
+ mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ }
+
+ subscriptionViewModel = SubscriptionInfoListViewModel(
+ context.applicationContext as Application)
+
+ allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow)
+ .collectLatestWithLifecycle(LocalLifecycleOwner.current) {
+ selectableSubscriptionInfoListRemember.clear()
+ selectableSubscriptionInfoListRemember.addAll(selectableSubscriptionInfoList)
+ callsSelectedId.intValue = defaultVoiceSubId
+ textsSelectedId.intValue = defaultSmsSubId
+ mobileDataSelectedId.intValue = defaultDataSubId
+ nonDdsRemember.intValue = nonDds
+ }
+
+ PageImpl(selectableSubscriptionInfoListRemember,
+ callsSelectedId,
+ textsSelectedId,
+ mobileDataSelectedId,
+ nonDdsRemember)
+ }
+
+ private fun allOfFlows(context: Context,
+ selectableSubscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) =
+ combine(
+ selectableSubscriptionInfoListFlow,
+ context.defaultVoiceSubscriptionFlow(),
+ context.defaultSmsSubscriptionFlow(),
+ context.defaultDefaultDataSubscriptionFlow(),
+ NetworkCellularGroupProvider::refreshUiStates,
+ ).flowOn(Dispatchers.Default)
+
+ fun refreshUiStates(
+ inputSelectableSubscriptionInfoList: List<SubscriptionInfo>,
+ inputDefaultVoiceSubId: Int,
+ inputDefaultSmsSubId: Int,
+ inputDefaultDateSubId: Int
+ ): Unit {
+ selectableSubscriptionInfoList = inputSelectableSubscriptionInfoList
+ defaultVoiceSubId = inputDefaultVoiceSubId
+ defaultSmsSubId = inputDefaultSmsSubId
+ defaultDataSubId = inputDefaultDateSubId
+ nonDds = if (defaultDataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ } else {
+ selectableSubscriptionInfoList
+ .filter { info ->
+ (info.simSlotIndex != -1) && (info.subscriptionId != defaultDataSubId)
+ }
+ .map { it.subscriptionId }
+ .firstOrNull() ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ }
+ }
+}
+
+@Composable
+fun PageImpl(selectableSubscriptionInfoList: List<SubscriptionInfo>,
+ defaultVoiceSubId: MutableIntState,
+ defaultSmsSubId: MutableIntState,
+ defaultDataSubId: MutableIntState,
+ nonDds: MutableIntState) {
+ val context = LocalContext.current
+ var activeSubscriptionInfoList: List<SubscriptionInfo> =
+ selectableSubscriptionInfoList.filter { subscriptionInfo ->
+ subscriptionInfo.simSlotIndex != -1
+ }
+ var subscriptionManager = context.getSystemService(SubscriptionManager::class.java)
+
+ val stringSims = stringResource(R.string.provider_network_settings_title)
+ RegularScaffold(title = stringSims) {
+ SimsSectionImpl(
+ context,
+ subscriptionManager,
+ selectableSubscriptionInfoList
+ )
+ PrimarySimSectionImpl(
+ subscriptionManager,
+ activeSubscriptionInfoList,
+ defaultVoiceSubId,
+ defaultSmsSubId,
+ defaultDataSubId,
+ nonDds
+ )
+ }
+}
+
+@Composable
+fun SimsSectionImpl(
+ context: Context,
+ subscriptionManager: SubscriptionManager?,
+ subscriptionInfoList: List<SubscriptionInfo>
+) {
+ val coroutineScope = rememberCoroutineScope()
+ for (subInfo in subscriptionInfoList) {
+ val checked = rememberSaveable() {
+ mutableStateOf(false)
+ }
+ //TODO: Add the Restricted TwoTargetSwitchPreference in SPA
+ TwoTargetSwitchPreference(remember {
+ object : SwitchPreferenceModel {
+ override val title = subInfo.displayName.toString()
+ override val summary = { subInfo.number }
+ override val checked = {
+ coroutineScope.launch {
+ withContext(Dispatchers.Default) {
+ checked.value = subscriptionManager?.isSubscriptionEnabled(
+ subInfo.subscriptionId)?:false
+ }
+ }
+ checked.value
+ }
+ override val onCheckedChange = { newChecked: Boolean ->
+ startToggleSubscriptionDialog(context, subInfo, newChecked)
+ }
+ }
+ }) {
+ startMobileNetworkSettings(context, subInfo)
+ }
+ }
+
+ // + add sim
+ if (showEuiccSettings(context)) {
+ RestrictedPreference(
+ model = object : PreferenceModel {
+ override val title = stringResource(id = R.string.mobile_network_list_add_more)
+ override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) }
+ override val onClick = {
+ startAddSimFlow(context)
+ }
+ },
+ restrictions = Restrictions(keys =
+ listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)),
+ )
+ }
+}
+
+@Composable
+fun PrimarySimSectionImpl(
+ subscriptionManager: SubscriptionManager?,
+ activeSubscriptionInfoList: List<SubscriptionInfo>,
+ callsSelectedId: MutableIntState,
+ textsSelectedId: MutableIntState,
+ mobileDataSelectedId: MutableIntState,
+ nonDds: MutableIntState
+) {
+ var state = rememberSaveable { mutableStateOf(false) }
+ var callsAndSmsList = remember {
+ mutableListOf(ListPreferenceOption(id = -1, text = "Loading"))
+ }
+ var dataList = remember {
+ mutableListOf(ListPreferenceOption(id = -1, text = "Loading"))
+ }
+
+ if (activeSubscriptionInfoList.size >= 2) {
+ state.value = true
+ callsAndSmsList.clear()
+ dataList.clear()
+ for (info in activeSubscriptionInfoList) {
+ var item = ListPreferenceOption(
+ id = info.subscriptionId,
+ text = "${info.displayName}"
+ )
+ callsAndSmsList.add(item)
+ dataList.add(item)
+ }
+ callsAndSmsList.add(ListPreferenceOption(
+ id = SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ text = stringResource(id = R.string.sim_calls_ask_first_prefs_title)
+ ))
+ } else {
+ // hide the primary sim
+ state.value = false
+ Log.d("NetworkCellularGroupProvider", "Hide primary sim")
+ }
+
+ if (state.value) {
+ val coroutineScope = rememberCoroutineScope()
+ var context = LocalContext.current
+ val telephonyManagerForNonDds: TelephonyManager? =
+ context.getSystemService(TelephonyManager::class.java)
+ ?.createForSubscriptionId(nonDds.intValue)
+ val automaticDataChecked = rememberSaveable() {
+ mutableStateOf(false)
+ }
+
+ Category(title = stringResource(id = R.string.primary_sim_title)) {
+ createPrimarySimListPreference(
+ stringResource(id = R.string.primary_sim_calls_title),
+ callsAndSmsList,
+ callsSelectedId,
+ ImageVector.vectorResource(R.drawable.ic_phone),
+ ) {
+ callsSelectedId.intValue = it
+ coroutineScope.launch {
+ setDefaultVoice(subscriptionManager, it)
+ }
+ }
+ createPrimarySimListPreference(
+ stringResource(id = R.string.primary_sim_texts_title),
+ callsAndSmsList,
+ textsSelectedId,
+ Icons.AutoMirrored.Outlined.Message,
+ ) {
+ textsSelectedId.intValue = it
+ coroutineScope.launch {
+ setDefaultSms(subscriptionManager, it)
+ }
+ }
+ createPrimarySimListPreference(
+ stringResource(id = R.string.mobile_data_settings_title),
+ dataList,
+ mobileDataSelectedId,
+ Icons.Outlined.DataUsage,
+ ) {
+ mobileDataSelectedId.intValue = it
+ coroutineScope.launch {
+ // TODO: to fix the WifiPickerTracker crash when create
+ // the wifiPickerTrackerHelper
+ setDefaultData(context,
+ subscriptionManager,
+ null/*wifiPickerTrackerHelper*/,
+ it)
+ }
+ }
+ }
+
+ val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title)
+ val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg)
+ SwitchPreference(remember {
+ object : SwitchPreferenceModel {
+ override val title = autoDataTitle
+ override val summary = { autoDataSummary }
+ override val changeable: () -> Boolean = {
+ nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ }
+ override val checked = {
+ coroutineScope.launch {
+ withContext(Dispatchers.Default) {
+ automaticDataChecked.value = telephonyManagerForNonDds != null
+ && telephonyManagerForNonDds.isMobileDataPolicyEnabled(
+ TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
+ }
+ }
+ automaticDataChecked.value
+ }
+ override val onCheckedChange: ((Boolean) -> Unit)? =
+ { newChecked: Boolean ->
+ coroutineScope.launch {
+ setAutomaticData(telephonyManagerForNonDds, newChecked)
+ }
+ }
+ }
+ })
+ }
+}
+
+private fun Context.defaultVoiceSubscriptionFlow(): Flow<Int> =
+ merge(
+ flowOf(null), // kick an initial value
+ broadcastReceiverFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)
+ ),
+ ).map { SubscriptionManager.getDefaultVoiceSubscriptionId() }
+ .conflate().flowOn(Dispatchers.Default)
+
+private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
+ merge(
+ flowOf(null), // kick an initial value
+ broadcastReceiverFlow(
+ IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED)
+ ),
+ ).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
+ .conflate().flowOn(Dispatchers.Default)
+
+private fun Context.defaultDefaultDataSubscriptionFlow(): Flow<Int> =
+ merge(
+ flowOf(null), // kick an initial value
+ broadcastReceiverFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ ),
+ ).map { SubscriptionManager.getDefaultDataSubscriptionId() }
+ .conflate().flowOn(Dispatchers.Default)
+
+private fun startToggleSubscriptionDialog(
+ context: Context,
+ subInfo: SubscriptionInfo,
+ newStatus: Boolean
+) {
+ SubscriptionUtil.startToggleSubscriptionDialogActivity(
+ context,
+ subInfo.subscriptionId,
+ newStatus
+ )
+}
+
+private fun startMobileNetworkSettings(context: Context, subInfo: SubscriptionInfo) {
+ MobileNetworkUtils.launchMobileNetworkSettings(context, subInfo)
+}
+
+private fun startAddSimFlow(context: Context) {
+ val intent = Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION)
+ intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true)
+ context.startActivity(intent)
+}
+
+private fun showEuiccSettings(context: Context): Boolean {
+ return MobileNetworkUtils.showEuiccSettings(context)
+}
+
+private suspend fun setDefaultVoice(
+ subscriptionManager: SubscriptionManager?,
+ subId: Int): Unit = withContext(Dispatchers.Default) {
+ subscriptionManager?.setDefaultVoiceSubscriptionId(subId)
+}
+
+private suspend fun setDefaultSms(
+ subscriptionManager: SubscriptionManager?,
+ subId: Int): Unit = withContext(Dispatchers.Default) {
+ subscriptionManager?.setDefaultSmsSubId(subId)
+}
+
+private suspend fun setDefaultData(context: Context,
+ subscriptionManager: SubscriptionManager?,
+ wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
+ subId: Int): Unit = withContext(Dispatchers.Default) {
+ subscriptionManager?.setDefaultDataSubId(subId)
+ MobileNetworkUtils.setMobileDataEnabled(
+ context,
+ subId,
+ true /* enabled */,
+ true /* disableOtherSubscriptions */)
+ if (wifiPickerTrackerHelper != null
+ && !wifiPickerTrackerHelper.isCarrierNetworkProvisionEnabled(subId)) {
+ wifiPickerTrackerHelper.setCarrierNetworkEnabled(true)
+ }
+}
+
+private suspend fun setAutomaticData(telephonyManager: TelephonyManager?, newState: Boolean): Unit =
+ withContext(Dispatchers.Default) {
+ telephonyManager?.setMobileDataPolicyEnabled(
+ TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
+ newState)
+ //TODO: setup backup calling
+ }
\ No newline at end of file
diff --git a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
index 7704f84..5752a4f 100644
--- a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
+++ b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
@@ -108,21 +108,22 @@
list,
callsSelectedId,
ImageVector.vectorResource(R.drawable.ic_phone),
- true
+ onIdSelected = { callsSelectedId.intValue = it }
)
createPrimarySimListPreference(
stringResource(id = R.string.primary_sim_texts_title),
list,
textsSelectedId,
Icons.AutoMirrored.Outlined.Message,
- true
+ onIdSelected = { textsSelectedId.intValue = it }
)
+
createPrimarySimListPreference(
- stringResource(id = R.string.mobile_data_settings_title),
- list,
- mobileDataSelectedId,
+ stringResource(id = R.string.mobile_data_settings_title),
+ list,
+ mobileDataSelectedId,
Icons.Outlined.DataUsage,
- true
+ onIdSelected = { mobileDataSelectedId.intValue = it }
)
val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title)
@@ -140,17 +141,18 @@
@Composable
fun createPrimarySimListPreference(
- title: String,
- list: List<ListPreferenceOption>,
- selectedId: MutableIntState,
- icon: ImageVector,
- enable: Boolean
+ title: String,
+ list: List<ListPreferenceOption>,
+ selectedId: MutableIntState,
+ icon: ImageVector,
+ enable: Boolean = true,
+ onIdSelected: (id: Int) -> Unit
) = ListPreference(remember {
object : ListPreferenceModel {
override val title = title
override val options = list
override val selectedId = selectedId
- override val onIdSelected: (id: Int) -> Unit = { selectedId.intValue = it }
+ override val onIdSelected = onIdSelected
override val icon = @Composable {
SettingsIcon(icon)
}
diff --git a/src/com/android/settings/wifi/WifiDialogActivity.java b/src/com/android/settings/wifi/WifiDialogActivity.java
index 7e901c2..eb3d88a 100644
--- a/src/com/android/settings/wifi/WifiDialogActivity.java
+++ b/src/com/android/settings/wifi/WifiDialogActivity.java
@@ -17,6 +17,7 @@
package com.android.settings.wifi;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.os.UserManager.DISALLOW_ADD_WIFI_CONFIG;
import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
import android.app.KeyguardManager;
@@ -122,7 +123,7 @@
}
super.onCreate(savedInstanceState);
- if (!isConfigWifiAllowed()) {
+ if (!isConfigWifiAllowed() || !isAddWifiConfigAllowed()) {
finish();
return;
}
@@ -393,6 +394,16 @@
return isConfigWifiAllowed;
}
+ @VisibleForTesting
+ boolean isAddWifiConfigAllowed() {
+ UserManager userManager = getSystemService(UserManager.class);
+ if (userManager != null && userManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)) {
+ Log.e(TAG, "The user is not allowed to add Wi-Fi configuration.");
+ return false;
+ }
+ return true;
+ }
+
private boolean hasWifiManager() {
if (mWifiManager != null) return true;
mWifiManager = getSystemService(WifiManager.class);
diff --git a/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java
index 53a87ba..7586954 100644
--- a/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/shortcuts/EditShortcutsPreferenceFragmentTest.java
@@ -51,6 +51,7 @@
import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
import com.android.settings.accessibility.AccessibilityUtil;
+import com.android.settings.accessibility.PreferredShortcuts;
import com.android.settings.testutils.shadow.SettingsShadowResources;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
@@ -369,6 +370,50 @@
});
}
+ @Test
+ public void fragmentResumed_preferredShortcutsUpdated() {
+ mFragmentScenario = createFragScenario(/* isInSuw= */ false);
+ mFragmentScenario.moveToState(Lifecycle.State.RESUMED);
+ // Move the fragment to the background
+ mFragmentScenario.moveToState(Lifecycle.State.CREATED);
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, TARGET, ShortcutConstants.UserShortcutType.SOFTWARE)
+ ).isEqualTo(ShortcutConstants.UserShortcutType.SOFTWARE);
+ // Update the chosen shortcut type to Volume keys while the fragment is in the background
+ ShortcutUtils.optInValueToSettings(
+ mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET);
+
+ mFragmentScenario.moveToState(Lifecycle.State.RESUMED);
+
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, TARGET, ShortcutConstants.UserShortcutType.SOFTWARE)
+ ).isEqualTo(ShortcutConstants.UserShortcutType.HARDWARE);
+ }
+
+ @Test
+ public void onVolumeKeysShortcutSettingChanged_preferredShortcutsUpdated() {
+ mFragmentScenario = createFragScenario(/* isInSuw= */ false);
+ mFragmentScenario.moveToState(Lifecycle.State.CREATED);
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, TARGET, ShortcutConstants.UserShortcutType.SOFTWARE)
+ ).isEqualTo(ShortcutConstants.UserShortcutType.SOFTWARE);
+
+ ShortcutUtils.optInValueToSettings(
+ mContext, ShortcutConstants.UserShortcutType.HARDWARE, TARGET);
+
+ // Calls onFragment so that the change to Setting is notified to its observer
+ mFragmentScenario.onFragment(fragment ->
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, TARGET, ShortcutConstants.UserShortcutType.SOFTWARE)
+ ).isEqualTo(ShortcutConstants.UserShortcutType.HARDWARE)
+ );
+
+ }
+
private void assertLaunchSubSettingWithCurrentTargetComponents(
String componentName, boolean isInSuw) {
Intent intent = shadowOf(mActivity.getApplication()).getNextStartedActivity();
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
index e30759a..22fec8f 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
@@ -16,27 +16,58 @@
package com.android.settings.dashboard.profileselector;
+import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
+
import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.UserInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.shadow.ShadowUserManager;
+
import org.junit.Before;
-import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
-@Ignore("b/313569889")
+@Config(shadows = {
+ ShadowUserManager.class,
+})
@RunWith(RobolectricTestRunner.class)
public class ProfileSelectLocationFragmentTest {
+ private static final String PERSONAL_PROFILE_NAME = "personal";
+ private static final String WORK_PROFILE_NAME = "work";
+ private static final String PRIVATE_PROFILE_NAME = "private";
+ @Rule
+ public final MockitoRule rule = MockitoJUnit.rule();
+ private ShadowUserManager mUserManager;
private ProfileSelectLocationFragment mProfileSelectLocationFragment;
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
- mProfileSelectLocationFragment = new ProfileSelectLocationFragment();
+ mUserManager = ShadowUserManager.getShadow();
+ mUserManager.addProfile(
+ new UserInfo(0, PERSONAL_PROFILE_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+ mUserManager.addProfile(
+ new UserInfo(1, WORK_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
+ mUserManager.addProfile(
+ new UserInfo(11, PRIVATE_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
+ mProfileSelectLocationFragment = spy(new ProfileSelectLocationFragment());
+ when(mProfileSelectLocationFragment.getContext()).thenReturn(
+ ApplicationProvider.getApplicationContext());
}
@Test
@@ -46,7 +77,7 @@
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PERSONAL);
assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt(
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.WORK);
- assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt(
+ assertThat(mProfileSelectLocationFragment.getFragments()[2].getArguments().getInt(
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PRIVATE);
}
}
diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java
new file mode 100644
index 0000000..f35fb17
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 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.development;
+
+import static com.android.settings.development.BluetoothLeAudioModePreferenceController
+ .LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.os.SystemProperties;
+
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothLeAudioModePreferenceControllerTest {
+
+ @Mock
+ private PreferenceScreen mPreferenceScreen;
+ @Mock
+ private DevelopmentSettingsDashboardFragment mFragment;
+ @Mock
+ private BluetoothAdapter mBluetoothAdapter;
+ @Mock
+ private ListPreference mPreference;
+
+ private Context mContext;
+ private BluetoothLeAudioModePreferenceController mController;
+ private String[] mListValues;
+ private String[] mListSummaries;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mListValues = mContext.getResources().getStringArray(
+ R.array.bluetooth_leaudio_mode_values);
+ mListSummaries = mContext.getResources().getStringArray(
+ R.array.bluetooth_leaudio_mode);
+ mController = spy(new BluetoothLeAudioModePreferenceController(mContext, mFragment));
+ when(mPreferenceScreen.findPreference(mController.getPreferenceKey()))
+ .thenReturn(mPreference);
+ mController.mBluetoothAdapter = mBluetoothAdapter;
+ mController.displayPreference(mPreferenceScreen);
+ }
+
+ @Test
+ public void onRebootDialogConfirmed_changeLeAudioMode_shouldSetLeAudioMode() {
+ mController.mChanged = true;
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
+ mController.mNewMode = mListValues[1];
+
+ mController.onRebootDialogConfirmed();
+ assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
+ .equals(mController.mNewMode)).isTrue();
+ }
+
+ @Test
+ public void onRebootDialogConfirmed_notChangeLeAudioMode_shouldNotSetLeAudioMode() {
+ mController.mChanged = false;
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
+ mController.mNewMode = mListValues[1];
+
+ mController.onRebootDialogConfirmed();
+ assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
+ .equals(mController.mNewMode)).isFalse();
+ }
+
+ @Test
+ public void onRebootDialogCanceled_shouldNotSetLeAudioMode() {
+ mController.mChanged = true;
+ SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
+ mController.mNewMode = mListValues[1];
+
+ mController.onRebootDialogCanceled();
+ assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
+ .equals(mController.mNewMode)).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/security/ScreenPinningSettingsTest.java b/tests/robotests/src/com/android/settings/security/ScreenPinningSettingsTest.java
new file mode 100644
index 0000000..045ef65
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/security/ScreenPinningSettingsTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
+import com.android.settingslib.search.SearchIndexableRaw;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = ShadowLockPatternUtils.class)
+public class ScreenPinningSettingsTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowLockPatternUtils.reset();
+ }
+
+ @Test
+ public void getDynamicRawDataToIndex_numericPassword_shouldIndexUnlockPinTitle() {
+ ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+
+ final List<SearchIndexableRaw> indexRaws =
+ ScreenPinningSettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true);
+
+ assertThat(indexRaws.size()).isEqualTo(1);
+ assertThat(indexRaws.get(0).title).isEqualTo(
+ mContext.getString(R.string.screen_pinning_unlock_pin));
+ }
+
+ @Test
+ public void getDynamicRawDataToIndex_alphabeticPassword_shouldIndexUnlockPasswordTitle() {
+ ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
+ DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC);
+
+ final List<SearchIndexableRaw> indexRaws =
+ ScreenPinningSettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true);
+
+ assertThat(indexRaws.size()).isEqualTo(1);
+ assertThat(indexRaws.get(0).title).isEqualTo(
+ mContext.getString(R.string.screen_pinning_unlock_password));
+ }
+
+ @Test
+ public void getDynamicRawDataToIndex_patternPassword_shouldIndexUnlockPatternTitle() {
+ ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
+ DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
+ ShadowLockPatternUtils.setIsLockPatternEnabled(
+ UserHandle.myUserId(), /* isLockPatternEnabled= */ true);
+
+ final List<SearchIndexableRaw> indexRaws =
+ ScreenPinningSettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true);
+
+ assertThat(indexRaws.size()).isEqualTo(1);
+ assertThat(indexRaws.get(0).title).isEqualTo(
+ mContext.getString(R.string.screen_pinning_unlock_pattern));
+ }
+
+ @Test
+ public void getDynamicRawDataToIndex_nonePassword_shouldIndexUnlockNoneTitle() {
+ ShadowLockPatternUtils.setKeyguardStoredPasswordQuality(
+ DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED);
+
+ final List<SearchIndexableRaw> indexRaws =
+ ScreenPinningSettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true);
+
+ assertThat(indexRaws.size()).isEqualTo(1);
+ assertThat(indexRaws.get(0).title).isEqualTo(
+ mContext.getString(R.string.screen_pinning_unlock_none));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java b/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
index ff0395d..886a4bc 100644
--- a/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.os.UserManager.DISALLOW_ADD_WIFI_CONFIG;
import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
import static com.android.settings.wifi.WifiDialogActivity.REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER;
@@ -50,16 +51,16 @@
import com.google.android.setupcompat.util.WizardManagerHelper;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
-@Ignore("b/314867581")
@RunWith(RobolectricTestRunner.class)
+@LooperMode(LooperMode.Mode.LEGACY)
public class WifiDialogActivityTest {
static final String CALLING_PACKAGE = "calling_package";
@@ -243,6 +244,20 @@
}
@Test
+ public void isAddWifiConfigAllowed_hasNoUserRestriction_returnTrue() {
+ when(mUserManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)).thenReturn(false);
+
+ assertThat(mActivity.isAddWifiConfigAllowed()).isTrue();
+ }
+
+ @Test
+ public void isAddWifiConfigAllowed_hasUserRestriction_returnFalse() {
+ when(mUserManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)).thenReturn(true);
+
+ assertThat(mActivity.isAddWifiConfigAllowed()).isFalse();
+ }
+
+ @Test
public void hasPermissionForResult_noCallingPackage_returnFalse() {
when(mActivity.getCallingPackage()).thenReturn(null);
diff --git a/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java b/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
index 0474f52..efea6fd 100644
--- a/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
+++ b/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowLockPatternUtils.java
@@ -50,6 +50,7 @@
private static Map<Integer, Boolean> sUserToVisiblePatternEnabledMap = new HashMap<>();
private static Map<Integer, Boolean> sUserToBiometricAllowedMap = new HashMap<>();
private static Map<Integer, Boolean> sUserToLockPatternEnabledMap = new HashMap<>();
+ private static Map<Integer, Integer> sKeyguardStoredPasswordQualityMap = new HashMap<>();
private static boolean sIsUserOwnsFrpCredential;
@@ -66,6 +67,7 @@
sUserToLockPatternEnabledMap.clear();
sDeviceEncryptionEnabled = false;
sIsUserOwnsFrpCredential = false;
+ sKeyguardStoredPasswordQualityMap.clear();
}
@Implementation
@@ -97,7 +99,7 @@
@Implementation
protected int getKeyguardStoredPasswordQuality(int userHandle) {
- return 1;
+ return sKeyguardStoredPasswordQualityMap.getOrDefault(userHandle, /* defaultValue= */ 1);
}
@Implementation
@@ -171,7 +173,7 @@
@Implementation
public boolean isLockPatternEnabled(int userId) {
- return sUserToBiometricAllowedMap.getOrDefault(userId, false);
+ return sUserToLockPatternEnabledMap.getOrDefault(userId, false);
}
public static void setIsLockPatternEnabled(int userId, boolean isLockPatternEnabled) {
@@ -238,4 +240,8 @@
public boolean isSeparateProfileChallengeEnabled(int userHandle) {
return false;
}
+
+ public static void setKeyguardStoredPasswordQuality(int quality) {
+ sKeyguardStoredPasswordQualityMap.put(UserHandle.myUserId(), quality);
+ }
}
diff --git a/tests/unit/src/com/android/settings/accessibility/PreferredShortcutsTest.java b/tests/unit/src/com/android/settings/accessibility/PreferredShortcutsTest.java
index 95a0b83..e7dfb5b 100644
--- a/tests/unit/src/com/android/settings/accessibility/PreferredShortcutsTest.java
+++ b/tests/unit/src/com/android/settings/accessibility/PreferredShortcutsTest.java
@@ -16,17 +16,30 @@
package com.android.settings.accessibility;
+import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+
import static com.google.common.truth.Truth.assertThat;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
+import android.provider.Settings;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.internal.accessibility.common.ShortcutConstants;
+import com.android.internal.accessibility.util.ShortcutUtils;
+
+import org.junit.AfterClass;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Set;
+
/** Tests for {@link PreferredShortcuts} */
@RunWith(AndroidJUnit4.class)
public class PreferredShortcutsTest {
@@ -39,8 +52,20 @@
private static final String CLASS_NAME_2 = PACKAGE_NAME_2 + ".test2";
private static final ComponentName COMPONENT_NAME_2 = new ComponentName(PACKAGE_NAME_2,
CLASS_NAME_2);
+ private static final ContentResolver sContentResolver =
+ ApplicationProvider.getApplicationContext().getContentResolver();
- private Context mContext = ApplicationProvider.getApplicationContext();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Before
+ public void setUp() {
+ clearShortcuts();
+ }
+
+ @AfterClass
+ public static void cleanUp() {
+ clearShortcuts();
+ }
@Test
public void retrieveUserShortcutType_fromSingleData_matchSavedType() {
@@ -71,4 +96,88 @@
assertThat(retrieveType).isEqualTo(type1);
}
+
+ @Test
+ public void updatePreferredShortcutsFromSetting_magnificationWithTripleTapAndVolumeKeyShortcuts_preferredShortcutsMatches() {
+ ShortcutUtils.optInValueToSettings(mContext, ShortcutConstants.UserShortcutType.HARDWARE,
+ MAGNIFICATION_CONTROLLER_NAME);
+ Settings.Secure.putInt(
+ sContentResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+ AccessibilityUtil.State.ON);
+
+ PreferredShortcuts.updatePreferredShortcutsFromSettings(mContext,
+ Set.of(MAGNIFICATION_CONTROLLER_NAME));
+ int expectedShortcutTypes = ShortcutConstants.UserShortcutType.HARDWARE
+ | ShortcutConstants.UserShortcutType.TRIPLETAP;
+
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, MAGNIFICATION_CONTROLLER_NAME,
+ ShortcutConstants.UserShortcutType.SOFTWARE))
+ .isEqualTo(expectedShortcutTypes);
+ }
+
+ @Test
+ public void updatePreferredShortcutsFromSetting_magnificationWithNoActiveShortcuts_noChangesOnPreferredShortcutTypes() {
+ int expectedShortcutTypes = ShortcutConstants.UserShortcutType.HARDWARE
+ | ShortcutConstants.UserShortcutType.SOFTWARE;
+ PreferredShortcuts.saveUserShortcutType(mContext,
+ new PreferredShortcut(MAGNIFICATION_CONTROLLER_NAME, expectedShortcutTypes));
+
+
+ PreferredShortcuts.updatePreferredShortcutsFromSettings(mContext,
+ Set.of(MAGNIFICATION_CONTROLLER_NAME));
+
+
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, MAGNIFICATION_CONTROLLER_NAME,
+ ShortcutConstants.UserShortcutType.SOFTWARE))
+ .isEqualTo(expectedShortcutTypes);
+ }
+
+ @Test
+ public void updatePreferredShortcutsFromSetting_multipleComponents_preferredShortcutsMatches() {
+ String target1 = COLOR_INVERSION_COMPONENT_NAME.flattenToString();
+ String target2 = DALTONIZER_COMPONENT_NAME.flattenToString();
+
+ Settings.Secure.putString(sContentResolver,
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, target1);
+ Settings.Secure.putString(sContentResolver,
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ target1 + ShortcutConstants.SERVICES_SEPARATOR + target2);
+
+ int target1ShortcutTypes = ShortcutConstants.UserShortcutType.HARDWARE
+ | ShortcutConstants.UserShortcutType.SOFTWARE;
+ int target2ShortcutTypes = ShortcutConstants.UserShortcutType.HARDWARE;
+
+ PreferredShortcuts.updatePreferredShortcutsFromSettings(mContext, Set.of(target1, target2));
+
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, target1,
+ ShortcutConstants.UserShortcutType.SOFTWARE))
+ .isEqualTo(target1ShortcutTypes);
+ assertThat(
+ PreferredShortcuts.retrieveUserShortcutType(
+ mContext, target2,
+ ShortcutConstants.UserShortcutType.SOFTWARE))
+ .isEqualTo(target2ShortcutTypes);
+ }
+
+ private static void clearShortcuts() {
+ Settings.Secure.putString(sContentResolver,
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "");
+ Settings.Secure.putString(sContentResolver,
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "");
+ Settings.Secure.putInt(
+ sContentResolver,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+ AccessibilityUtil.State.OFF);
+ Settings.Secure.putInt(
+ sContentResolver,
+ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
+ AccessibilityUtil.State.OFF);
+ }
}
diff --git a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
index 6678603..080534e 100644
--- a/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
+++ b/tests/unit/src/com/android/settings/network/telephony/NetworkSelectSettingsTest.java
@@ -290,7 +290,7 @@
List<String> testSatellitePlmn = new ArrayList<>(Arrays.asList("123232", "123235"));
doReturn(testSatellitePlmn).when(
- mNetworkSelectSettings).getAllSatellitePlmnsForCarrierWrapper();
+ mNetworkSelectSettings).getSatellitePlmnsForCarrierWrapper();
/* Expect filter out satellite plmns when
KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL is true, and there is available
@@ -318,13 +318,13 @@
List<String> testSatellitePlmn = new ArrayList<>(Arrays.asList("123232", "123235"));
doReturn(testSatellitePlmn).when(
- mNetworkSelectSettings).getAllSatellitePlmnsForCarrierWrapper();
+ mNetworkSelectSettings).getSatellitePlmnsForCarrierWrapper();
// Expect no filter out when there is no available satellite plmns.
mNetworkSelectSettings.onCreateInitialization();
testSatellitePlmn = new ArrayList<>();
doReturn(testSatellitePlmn).when(
- mNetworkSelectSettings).getAllSatellitePlmnsForCarrierWrapper();
+ mNetworkSelectSettings).getSatellitePlmnsForCarrierWrapper();
mNetworkSelectSettings.onCreateInitialization();
List<CellInfo> testList = Arrays.asList(
createLteCellInfo(true, 123, "123", "232", "CarrierA"),
@@ -356,7 +356,7 @@
List<String> testSatellitePlmn = new ArrayList<>(Arrays.asList("123232", "123235"));
doReturn(testSatellitePlmn).when(
- mNetworkSelectSettings).getAllSatellitePlmnsForCarrierWrapper();
+ mNetworkSelectSettings).getSatellitePlmnsForCarrierWrapper();
// Expect no filter out when KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL is false.
config.putBoolean(