Merge Android 24Q2 Release (ab/11526283) to aosp-main-future

Bug: 337098550
Merged-In: I96574a79eba581db95d387f0d9c9fde2e004c41c
Change-Id: Ib9f2c742f8aa72651ef9eca80a716dd94b9041ea
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aed51f3..f062ad9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -234,6 +234,28 @@
             </intent-filter>
         </receiver>
 
+        <receiver
+            android:name=".development.Enable16KBootReceiver"
+            android:enabled="true"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+
+        <service
+            android:name=".development.PageAgnosticNotificationService"
+            android:enabled="true"
+            android:exported="false"
+            android:permission="android.permission.POST_NOTIFICATIONS"/>
+
+        <activity android:name=".development.PageAgnosticWarningActivity"
+                  android:enabled="true"
+                  android:launchMode="singleTask"
+                  android:taskAffinity=""
+                  android:excludeFromRecents="true"
+                  android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight"/>
+
         <activity android:name=".SubSettings"
                   android:exported="false"
                   android:theme="@style/Theme.SubSettings"
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
index d511d04..f7b4881 100644
--- a/aconfig/Android.bp
+++ b/aconfig/Android.bp
@@ -6,7 +6,7 @@
 aconfig_declarations {
     name: "aconfig_settings_flags",
     package: "com.android.settings.flags",
-    container: "system",
+    container: "system_ext",
     srcs: [
         "*.aconfig",
     ],
diff --git a/aconfig/development/settings_core_flag_declarations.aconfig b/aconfig/development/settings_core_flag_declarations.aconfig
index b73b026..fec67f6 100644
--- a/aconfig/development/settings_core_flag_declarations.aconfig
+++ b/aconfig/development/settings_core_flag_declarations.aconfig
@@ -2,6 +2,13 @@
 container: "system"
 
 flag {
+  name: "a2dp_offload_codec_extensibility_settings"
+  namespace: "bluetooth"
+  description: "Feature flag for Bluetooth Audio Codec extensibility in Settings"
+  bug: "323319530"
+}
+
+flag {
   name: "deprecate_list_activity"
   namespace: "android_settings"
   description: "Feature flag for deprecating ListActivity in Settings"
diff --git a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
index 6e02bfe..d0e5850 100644
--- a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
+++ b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 # NOTE: Don't add new accessibility flags here, since the package name doesn't follow
 # the best practice for setting's feature flag go/settings-trunk-stable
diff --git a/aconfig/settings_biometrics_framework_flag_declarations.aconfig b/aconfig/settings_biometrics_framework_flag_declarations.aconfig
index 4355ed1..e787da0 100644
--- a/aconfig/settings_biometrics_framework_flag_declarations.aconfig
+++ b/aconfig/settings_biometrics_framework_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "biometric_settings_provider"
diff --git a/aconfig/settings_biometrics_integration_declarations.aconfig b/aconfig/settings_biometrics_integration_declarations.aconfig
index ea3ac19..66d794b 100644
--- a/aconfig/settings_biometrics_integration_declarations.aconfig
+++ b/aconfig/settings_biometrics_integration_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "sfps_enroll_refinement"
diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig
index 3e771cd..3a21a53 100644
--- a/aconfig/settings_bluetooth_declarations.aconfig
+++ b/aconfig/settings_bluetooth_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_offload_bluetooth_operations_to_background_thread"
diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig
index 49dd4b4..3d2e9f5 100644
--- a/aconfig/settings_connecteddevice_flag_declarations.aconfig
+++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_subsequent_pair_settings_integration"
diff --git a/aconfig/settings_development_flag_declarations.aconfig b/aconfig/settings_development_flag_declarations.aconfig
index 318f862..b9a084f 100644
--- a/aconfig/settings_development_flag_declarations.aconfig
+++ b/aconfig/settings_development_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
diff --git a/aconfig/settings_display_flag_declarations.aconfig b/aconfig/settings_display_flag_declarations.aconfig
index 9fe587b..52b9603 100644
--- a/aconfig/settings_display_flag_declarations.aconfig
+++ b/aconfig/settings_display_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
     name: "protect_screen_timeout_with_auth"
diff --git a/aconfig/settings_experience_flag_declarations.aconfig b/aconfig/settings_experience_flag_declarations.aconfig
index e6cb924..9fe3f32 100644
--- a/aconfig/settings_experience_flag_declarations.aconfig
+++ b/aconfig/settings_experience_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
     name: "new_apn_page_enabled"
diff --git a/aconfig/settings_flag_declarations.aconfig b/aconfig/settings_flag_declarations.aconfig
index 9c3a7e1..0747d85 100644
--- a/aconfig/settings_flag_declarations.aconfig
+++ b/aconfig/settings_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
     name: "show_factory_reset_cancel_button"
diff --git a/aconfig/settings_globalintl_flag_declarations.aconfig b/aconfig/settings_globalintl_flag_declarations.aconfig
index 95202d3..48f3a29 100644
--- a/aconfig/settings_globalintl_flag_declarations.aconfig
+++ b/aconfig/settings_globalintl_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
     name: "terms_of_address_enabled"
diff --git a/aconfig/settings_notification_flag_declarations.aconfig b/aconfig/settings_notification_flag_declarations.aconfig
index bdb6573..f2ef428 100644
--- a/aconfig/settings_notification_flag_declarations.aconfig
+++ b/aconfig/settings_notification_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "dedupe_dnd_settings_channels"
diff --git a/aconfig/settings_onboarding_experience_flag_declarations.aconfig b/aconfig/settings_onboarding_experience_flag_declarations.aconfig
index 8d58d40..830c101 100644
--- a/aconfig/settings_onboarding_experience_flag_declarations.aconfig
+++ b/aconfig/settings_onboarding_experience_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_sound_backup"
diff --git a/aconfig/settings_panel_flag_declarations.aconfig b/aconfig/settings_panel_flag_declarations.aconfig
index efab83e..2ad5d3b 100644
--- a/aconfig/settings_panel_flag_declarations.aconfig
+++ b/aconfig/settings_panel_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_volume_plus_quick_settings"
diff --git a/aconfig/settings_perform_backup_tasks_flag_declarations.aconfig b/aconfig/settings_perform_backup_tasks_flag_declarations.aconfig
index d060e24..dcb876b 100644
--- a/aconfig/settings_perform_backup_tasks_flag_declarations.aconfig
+++ b/aconfig/settings_perform_backup_tasks_flag_declarations.aconfig
@@ -1,9 +1,9 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_perform_backup_tasks_in_settings"
   namespace: "backstage_power"
   description: "Enable the Perform Backup Tasks screen in Settings"
   bug: "320563660"
-}
\ No newline at end of file
+}
diff --git a/aconfig/settings_security_flag_declarations.aconfig b/aconfig/settings_security_flag_declarations.aconfig
index 3684212..e27f835 100644
--- a/aconfig/settings_security_flag_declarations.aconfig
+++ b/aconfig/settings_security_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "protect_lock_after_timeout_with_auth"
diff --git a/aconfig/settings_telephony_flag_declarations.aconfig b/aconfig/settings_telephony_flag_declarations.aconfig
index dab1b45..c12de7a 100644
--- a/aconfig/settings_telephony_flag_declarations.aconfig
+++ b/aconfig/settings_telephony_flag_declarations.aconfig
@@ -1,5 +1,5 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
     name: "remove_key_hide_enable_2g"
diff --git a/aconfig/settings_threadnetwork_flag_declarations.aconfig b/aconfig/settings_threadnetwork_flag_declarations.aconfig
new file mode 100644
index 0000000..9966467
--- /dev/null
+++ b/aconfig/settings_threadnetwork_flag_declarations.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.settings.flags"
+container: "system_ext"
+
+flag {
+    name: "thread_settings_enabled"
+    namespace: "thread_network"
+    description: "Controls whether the Thread Settings UX is displayed"
+    bug: "329384658"
+}
diff --git a/aconfig/settings_voice_activation_apps_flag_declarations.aconfig b/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
index a18e8c5..9ca3525 100644
--- a/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
+++ b/aconfig/settings_voice_activation_apps_flag_declarations.aconfig
@@ -1,9 +1,9 @@
 package: "com.android.settings.flags"
-container: "system"
+container: "system_ext"
 
 flag {
   name: "enable_voice_activation_apps_in_settings"
   namespace: "permissions"
   description: "Enable voice activation apps in Settings"
   bug: "303727896"
-}
\ No newline at end of file
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 55c48af..3ae2596 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -178,6 +178,9 @@
     <!-- Description for text in accessibility hearing aids footer. [CHAR LIMIT=NONE] -->
     <string name="bluetooth_audio_routing_footer_summary">By default, audio output is determined by individual apps</string>
 
+    <!-- Bluetooth audio codec related settings. Title of the default audio codec selection. [CHAR LIMIT=60] -->
+    <string name="bluetooth_audio_codec_default_selection">Use System Selection (Default)</string>
+
     <!--Bluetooth settings screen, summary text for Bluetooth device with no name -->
     <string name="bluetooth_device">Unnamed Bluetooth device</string>
     <!--Bluetooth settings screen, text that appears in heading bar when scanning for devices -->
@@ -11937,21 +11940,83 @@
     <!-- Developer Settings: Search keywords for the Profile HWUI rendering. [CHAR_LIMIT=NONE] -->
     <string name="track_frame_time_keywords">GPU</string>
 
-    <!-- setting Checkbox title whether to boot with 16K page size [CHAR_LIMIT=35] -->
-    <string name="enable_16k_pages">Boot with 16K page size</string>
+    <!-- setting Checkbox title whether to boot with 16KB page size [CHAR_LIMIT=35] -->
+    <string name="enable_16k_pages">Boot with 16KB page size</string>
     <!-- setting Checkbox summary whether to boot with 16K page size[CHAR_LIMIT=50] -->
-    <string name="enable_16k_pages_summary">Boot device using 16K page size supported kernel</string>
+    <string name="enable_16k_pages_summary">Boot device using 16KB page size supported kernel</string>
     <!-- Confirmation dialog title to ensure user wishes to enable 16K page size -->
-    <string name="confirm_enable_16k_pages_title">Reboot with 16KB pages compatible kernel?</string>
-    <!-- Warning dialog message to confirm user wishes to enable 16K page size -->
-    <string name="confirm_enable_16k_pages_text">WARNING: Some applications may not be compatible with this mode. Device will reboot after confirmation.</string>
+    <string name="confirm_enable_16k_pages_title">Switch from 4KB mode to 16KB mode</string>
+    <!-- Warning dialog message to confirm user wishes to enable 16KB page size -->
+    <string name="confirm_enable_16k_pages_text">You are in the page-agnostic mode running a 4KB kernel, and you are switching to the 16KB mode.
+        Software integrity cannot be guaranteed in this mode, and any data stored on the phone while the bootloader is unlocked may be at risk.
+        This will reboot the device. Some features will be disabled in these modes, so some applications may not work.
+        To return the device to production mode, you would need to then, switch back to the 4KB mode and lock the bootloader,
+        which factory resets the device. After the device successfully boots into Android, disable OEM unlocking in Developer options.</string>
     <!-- dialog title to confirm user wishes to revert to 4k page size kernel -->
-    <string name="confirm_enable_4k_pages_title">Reboot with 4KB pages compatible kernel?</string>
+    <string name="confirm_enable_4k_pages_title">Switch from 16KB mode to 4KB mode</string>
     <!-- dialog message to confirm user wishes to enable 4K page size -->
-    <string name="confirm_enable_4k_pages_text">Device will reboot after confirmation.</string>
+    <string name="confirm_enable_4k_pages_text">You are in the page-agnostic mode running a 16KB kernel, and you are switching to the 4KB mode.
+        Software integrity cannot be guaranteed in this mode, and any data stored on the phone while the bootloader is unlocked may be at risk.
+        This will reboot the device. Some features will be disabled in these modes, so some applications may not work. To return the device to production mode,
+        you would need to then lock the bootloader, which factory resets the device. After the device successfully boots into Android, disable OEM unlocking in Developer options.</string>
     <!-- Toast message when 16k OTA update fails -->
     <string name="toast_16k_update_failed_text">Failed to update kernel to 16KB pages compatible kernel.</string>
     <string name="progress_16k_ota_title">Applying change</string>
+    <!-- Confirmation dialog title and text to reformat data to ext4 -->
+    <string name="confirm_format_ext4_title">Reformat device to ext4? (required for 16KB mode)</string>
+    <string name="confirm_format_ext4_text">This device’s data partition needs to be converted to ext4 before using the 16KB developer option.
+        Software integrity cannot be guaranteed in this mode, and any data stored on the phone while the bootloader is unlocked may be at risk.
+        Activating the 16KB option will require one more reboot after this. Once you are in this mode, you can switch back and forth between 4KB and 16KB mode with a single reboot.
+        Some features will be disabled in these modes, so some applications may not work. To return the device to production mode, you would need to switch back to 4KB mode and
+        then lock the bootloader, which factory resets the device. After the device successfully boots into Android, disable OEM unlocking in Developer options.
+        The device will be wiped and the filesystem will be changed to ext4 after confirmation. After this completes, please come back to enable 16KB again.
+    </string>
+    <!-- Text for confirmation buttion for ext4 -->
+    <string name="confirm_ext4_button_text">Erase all data</string>
+    <!-- Toast on failure to reformat data to ext4 -->
+    <string name="format_ext4_failure_toast">Failed to reformat and wipe the data partition to ext4.</string>
+    <!-- Dialog to OEM unlock the device before using 16K developer option -->
+    <string name="confirm_oem_unlock_for_16k_title">Bootloader Unlock Required for 16KB Mode</string>
+    <string name="confirm_oem_unlock_for_16k_text">This device needs to have the bootloader unlocked before using the 16KB developer option.
+        Software integrity cannot be guaranteed in this mode, and any data stored on the phone while the bootloader is unlocked may be at risk.
+        All user data and settings will be wiped when activating 16KB mode. Once the bootloader is unlocked, activating the 16KB option will require two reboots.
+        Once you are in this mode, you can switch back and forth between 4KB and 16KB mode with a single reboot. Some features will be disabled in these modes,
+        so some applications may not work. To return the device to production mode, you would need to switch back to 4KB mode and then OEM/bootloader lock (which factory resets) the device.
+        Please unlock the bootloader and try again. You can see instructions for how to do this at
+        &lt;a href=\"https://source.android.com/docs/core/architecture/bootloader/locking_unlocking\"&gt;https://source.android.com/docs/core/architecture/bootloader/locking_unlocking&lt;/a&gt;
+    </string>
+    <!-- persistent notification 4k page agnostic mode title -->
+    <string name="page_agnostic_4k_pages_title">Using 4KB page-agnostic mode</string>
+    <!-- persistent notification 4k page agnostic mode text -->
+    <string name="page_agnostic_4k_pages_text_short">You are in the 4KB mode of the page-agnostic mode. Software integrity cannot be guaranteed in this mode,
+        and any data stored on the phone while the bootloader is unlocked may be at risk. Some features will be disabled in these modes, so some applications may not work.
+        In order to re-enter the production mode, you must lock the bootloader of the device. Tap to read more.</string>
+    <!-- persistent notification 16k page agnostic mode title -->
+    <string name="page_agnostic_4k_pages_text">You are in the 4KB mode of the page-agnostic mode. Software integrity cannot be guaranteed in this mode,
+        and any data stored on the phone while the bootloader is unlocked may be at risk. Some features will be disabled in these modes, so some applications may not work.
+        In order to re-enter the production mode, you must lock the bootloader of the device. This would factory reset the device again and restore it to production settings.
+        After the device successfully boots into Android, disable OEM unlocking in Developer options.
+        If the device fails to boot into Android or is unstable, re-flash the device with the latest factory images from
+        &lt;a href=\"https://developers.google.com/android/images\"&gt;https://developers.google.com/android/images&lt;/a&gt;
+        or use &lt;a href=\"https://flash.android.com/back-to-public\"&gt;https://flash.android.com/back-to-public&lt;/a&gt;
+        and select \'Wipe Device\', \'Lock Bootloader\' and \'Force Flash all partitions\'.</string>
+    <!-- persistent notification 16k page agnostic mode title -->
+    <string name="page_agnostic_16k_pages_title">Using 16KB page-agnostic mode</string>
+    <!-- persistent notification 16k page agnostic mode text -->
+    <string name="page_agnostic_16k_pages_text_short">You are in the 16KB mode of the page-agnostic mode. Software integrity cannot be guaranteed in this mode,
+        and any data stored on the phone while the bootloader is unlocked may be at risk. Some features will be disabled in these modes, so some applications may not work.
+        In order to re-enter the production mode, you must, switch back to 4K mode and then lock the bootloader of the device. Tap to read more.</string>
+    <string name="page_agnostic_16k_pages_text">You are in the 16KB mode of the page-agnostic mode. Software integrity cannot be guaranteed in this mode,
+        and any data stored on the phone while the bootloader is unlocked may be at risk. Some features will be disabled in these modes, so some applications may not work.
+        In order to re-enter the production mode, you must, switch back to 4K mode and then lock the bootloader of the device. This would factory reset the device again and
+        restore it to production settings. After the device successfully boots into Android, disable OEM unlocking in Developer options.
+        If the device fails to boot into Android or is unstable, re-flash the device with the latest factory images from
+        &lt;a href=\"https://developers.google.com/android/images\"&gt;https://developers.google.com/android/images&lt;/a&gt;
+        or use &lt;a href=\"https://flash.android.com/back-to-public\"&gt;https://flash.android.com/back-to-public&lt;/a&gt;
+        and select \'Wipe Device\', \'Lock Bootloader\' and \'Force Flash all partitions\'.</string>
+    <string name="page_agnostic_notification_channel_name">16KB Page-agnostic Mode</string>
+    <string name="page_agnostic_notification_action">Read more</string>
+
     <!-- DSU Loader. Do not translate. -->
 
     <string name="dsu_loader_title" translatable="false">DSU Loader</string>
@@ -12349,11 +12414,17 @@
     <!-- Title for Thread network preference [CHAR_LIMIT=60] -->
     <string name="thread_network_settings_title">Thread</string>
 
-    <!-- Summary for Thread network preference. [CHAR_LIMIT=NONE]-->
-    <string name="thread_network_settings_summary">Connect to compatible devices using Thread for a seamless smart home experience</string>
+    <!-- Title for Thread network settings main switch [CHAR_LIMIT=60] -->
+    <string name="thread_network_settings_main_switch_title">Use Thread</string>
 
-    <!-- Summary for Thread network preference when airplane mode is enabled. [CHAR_LIMIT=NONE]-->
-    <string name="thread_network_settings_summary_airplane_mode">Turn off airplane mode to use Thread</string>
+    <!-- Title for Thread network settings footer [CHAR_LIMIT=NONE] -->
+    <string name="thread_network_settings_footer_title">Thread helps connect your smart home devices, boosting efficiency, and performance.\n\nWhen enabled, this device is eligible to join a Thread network, allowing control of Matter supported devices through this phone.</string>
+
+    <!-- Text for Thread network settings learn more link [CHAR_LIMIT=NONE] -->
+    <string name="thread_network_settings_learn_more">Learn more about Thread</string>
+
+    <!-- URL for Thread network settings learn more link [CHAR_LIMIT=NONE] -->
+    <string name="thread_network_settings_learn_more_link" translatable="false">https://developers.home.google.com</string>
 
     <!-- Label for the camera use toggle [CHAR LIMIT=40] -->
     <string name="camera_toggle_title">Camera access</string>
diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml
index 87db619..68b4c04 100644
--- a/res/xml/connected_devices_advanced.xml
+++ b/res/xml/connected_devices_advanced.xml
@@ -47,6 +47,17 @@
         settings:keywords="@string/keywords_wifi_display_settings" />
 
     <com.android.settingslib.RestrictedPreference
+        android:fragment="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFragment"
+        android:key="thread_network_settings"
+        android:title="@string/thread_network_settings_title"
+        android:icon="@*android:drawable/ic_thread_network"
+        android:order="-5"
+        settings:searchable="false"
+        settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFragmentController"
+        settings:userRestriction="no_thread_network"
+        settings:useAdminDisabledSummary="true"/>
+
+    <com.android.settingslib.RestrictedPreference
         android:fragment="com.android.settings.print.PrintSettingsFragment"
         android:icon="@*android:drawable/ic_settings_print"
         android:key="connected_device_printing"
@@ -63,15 +74,6 @@
         settings:useAdminDisabledSummary="true"
         settings:userRestriction="no_ultra_wideband_radio" />
 
-    <com.android.settingslib.RestrictedSwitchPreference
-        android:key="thread_network_settings"
-        android:title="@string/thread_network_settings_title"
-        android:order="110"
-        android:summary="@string/summary_placeholder"
-        settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController"
-        settings:userRestriction="no_thread_network"
-        settings:useAdminDisabledSummary="true"/>
-
     <PreferenceCategory
         android:key="dashboard_tile_placeholder"
         android:order="-8" />
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index c0b6560..72142fc 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -423,6 +423,11 @@
             android:positiveButtonText=""
             android:negativeButtonText="@string/dlg_ok"/>
 
+        <ListPreference
+            android:key="bluetooth_audio_codec_settings_list"
+            android:title="@string/bluetooth_select_a2dp_codec_type"
+            android:dialogTitle="@string/bluetooth_select_a2dp_codec_type_dialog_title"/>
+
         <com.android.settings.development.bluetooth.BluetoothSampleRateDialogPreference
             android:key="bluetooth_sample_rate_settings"
             android:title="@string/bluetooth_select_a2dp_codec_sample_rate"
diff --git a/res/xml/mobile_network_settings.xml b/res/xml/mobile_network_settings.xml
index 1e43ef0..947f391 100644
--- a/res/xml/mobile_network_settings.xml
+++ b/res/xml/mobile_network_settings.xml
@@ -37,6 +37,7 @@
         <ListPreference
             android:key="calls_preference"
             android:title="@string/calls_preference"
+            android:summary="@string/summary_placeholder"
             android:enabled="false"
             settings:controller="com.android.settings.network.telephony.CallsDefaultSubscriptionController"
             settings:allowDividerAbove="true"/>
@@ -44,6 +45,7 @@
         <ListPreference
             android:key="sms_preference"
             android:title="@string/sms_preference"
+            android:summary="@string/summary_placeholder"
             android:enabled="false"
             settings:controller="com.android.settings.network.telephony.SmsDefaultSubscriptionController"/>
 
diff --git a/res/xml/thread_network_settings.xml b/res/xml/thread_network_settings.xml
new file mode 100644
index 0000000..549d650
--- /dev/null
+++ b/res/xml/thread_network_settings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+  -->
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/thread_network_settings_title">
+
+    <com.android.settingslib.widget.MainSwitchPreference
+        android:key="toggle_thread_network"
+        android:title="@string/thread_network_settings_main_switch_title"
+        settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkToggleController"/>
+
+    <com.android.settingslib.widget.FooterPreference
+        android:key="thread_network_settings_footer"
+        android:title="@string/thread_network_settings_footer_title"
+        android:selectable="false"
+        settings:searchable="false"
+        settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFooterController"/>
+</PreferenceScreen>
diff --git a/src/com/android/settings/accessibility/OWNERS b/src/com/android/settings/accessibility/OWNERS
index 3bd156b..24ff9fd 100644
--- a/src/com/android/settings/accessibility/OWNERS
+++ b/src/com/android/settings/accessibility/OWNERS
@@ -1,3 +1,6 @@
+# The Android Accessibility team should approve all changes to Settings > Accessibility content.
+set noparent
+
 # Default reviewers for this and subdirectories.
 chunkulin@google.com
 danielnorman@google.com
@@ -8,5 +11,9 @@
 # Legacy owner(s).
 menghanli@google.com #{LAST_RESORT_SUGGESTION}
 
-per-file HapticFeedbackIntensityPreferenceController.java = michaelwr@google.com
-per-file *Vibration* = michaelwr@google.com
+# Core Settings owner for emergency changes.
+cipson@google.com #{LAST_RESORT_SUGGESTION}
+
+# Partner-team files
+per-file *Haptic* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
+per-file *Vibrat* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/src/com/android/settings/biometrics/OWNERS b/src/com/android/settings/biometrics/OWNERS
index cb0d034..f804b0f 100644
--- a/src/com/android/settings/biometrics/OWNERS
+++ b/src/com/android/settings/biometrics/OWNERS
@@ -1,3 +1,6 @@
+# The Android Biometric team should approve all changes to biometrics subdirectories.
+set noparent
+
 graciecheng@google.com
 ilyamaty@google.com
 jaggies@google.com
diff --git a/src/com/android/settings/biometrics2/OWNERS b/src/com/android/settings/biometrics2/OWNERS
index a257ed8..ac806eb 100644
--- a/src/com/android/settings/biometrics2/OWNERS
+++ b/src/com/android/settings/biometrics2/OWNERS
@@ -1 +1,4 @@
+# The Android Biometric team should approve all changes to biometrics2 subdirectories.
+set noparent
+
 include /src/com/android/settings/biometrics/OWNERS
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt
new file mode 100644
index 0000000..583706a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.threadnetwork
+
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/**
+ * A testable interface for [ThreadNetworkController] which is `final`.
+ *
+ * We are in a awkward situation that Android API guideline suggest `final` for API classes
+ * while Robolectric test is being deprecated for platform testing (See
+ * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's
+ * conflicting with the default "mockito-target" which is somehow indirectly depended by the
+ * `SettingsUnitTests` target.
+ */
+@VisibleForTesting
+interface BaseThreadNetworkController {
+    fun setEnabled(
+        enabled: Boolean,
+        executor: Executor,
+        receiver: OutcomeReceiver<Void?, ThreadNetworkException>
+    )
+
+    fun registerStateCallback(executor: Executor, callback: StateCallback)
+
+    fun unregisterStateCallback(callback: StateCallback)
+}
\ No newline at end of file
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt
new file mode 100644
index 0000000..1e3b624
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.util.Log
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.HelpUtils
+import com.android.settingslib.widget.FooterPreference
+
+/**
+ * The footer preference controller for Thread settings in
+ * "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkFooterController(
+    context: Context,
+    preferenceKey: String
+) : BasePreferenceController(context, preferenceKey) {
+    override fun getAvailabilityStatus(): Int {
+        // The thread_network_settings screen won't be displayed and it doesn't matter if this
+        // controller always return AVAILABLE
+        return AVAILABLE
+    }
+
+    override fun displayPreference(screen: PreferenceScreen) {
+        val footer: FooterPreference? = screen.findPreference(KEY_PREFERENCE_FOOTER)
+        if (footer != null) {
+            footer.setLearnMoreAction { _ -> openLocaleLearnMoreLink() }
+            footer.setLearnMoreText(mContext.getString(R.string.thread_network_settings_learn_more))
+        }
+    }
+
+    private fun openLocaleLearnMoreLink() {
+        val intent = HelpUtils.getHelpIntent(
+            mContext,
+            mContext.getString(R.string.thread_network_settings_learn_more_link),
+            mContext::class.java.name
+        )
+        if (intent != null) {
+            mContext.startActivity(intent)
+        } else {
+            Log.w(TAG, "HelpIntent is null")
+        }
+    }
+
+    companion object {
+        private const val TAG = "ThreadNetworkSettings"
+        private const val KEY_PREFERENCE_FOOTER = "thread_network_settings_footer"
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt
new file mode 100644
index 0000000..fd385d7
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.threadnetwork
+
+import android.app.settings.SettingsEnums
+import com.android.settings.R
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.search.BaseSearchIndexProvider
+import com.android.settingslib.search.SearchIndexable
+
+/** The fragment for Thread settings in "Connected devices > Connection preferences > Thread". */
+@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
+class ThreadNetworkFragment : DashboardFragment() {
+    override fun getPreferenceScreenResId() = R.xml.thread_network_settings
+
+    override fun getLogTag() = "ThreadNetworkFragment"
+
+    override fun getMetricsCategory() = SettingsEnums.CONNECTED_DEVICE_PREFERENCES_THREAD
+
+    companion object {
+        /** For Search. */
+        @JvmField
+        val SEARCH_INDEX_DATA_PROVIDER = BaseSearchIndexProvider(R.xml.thread_network_settings)
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt
new file mode 100644
index 0000000..beb824a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.flags.Flags
+import java.util.concurrent.Executor
+
+/**
+ * The fragment controller for Thread settings in
+ * "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkFragmentController @VisibleForTesting constructor(
+    context: Context,
+    preferenceKey: String,
+    private val executor: Executor,
+    private val threadController: BaseThreadNetworkController?
+) : BasePreferenceController(context, preferenceKey), LifecycleEventObserver {
+    private val stateCallback: StateCallback
+    private var threadEnabled = false
+    private var preference: Preference? = null
+
+    constructor(context: Context, preferenceKey: String) : this(
+        context,
+        preferenceKey,
+        ContextCompat.getMainExecutor(context),
+        ThreadNetworkUtils.getThreadNetworkController(context)
+    )
+
+    init {
+        stateCallback = newStateCallback()
+    }
+
+    override fun getAvailabilityStatus(): Int {
+        return if (!Flags.threadSettingsEnabled()) {
+            CONDITIONALLY_UNAVAILABLE
+        } else if (threadController == null) {
+            UNSUPPORTED_ON_DEVICE
+        } else {
+            AVAILABLE
+        }
+    }
+
+    override fun getSummary(): CharSequence {
+        return if (threadEnabled) {
+            mContext.getText(R.string.switch_on_text)
+        } else {
+            mContext.getText(R.string.switch_off_text)
+        }
+    }
+
+    override fun displayPreference(screen: PreferenceScreen) {
+        super.displayPreference(screen)
+        preference = screen.findPreference(preferenceKey)
+    }
+
+    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+        if (threadController == null) {
+            return
+        }
+
+        when (event) {
+            Lifecycle.Event.ON_START ->
+                threadController.registerStateCallback(executor, stateCallback)
+
+            Lifecycle.Event.ON_STOP ->
+                threadController.unregisterStateCallback(stateCallback)
+
+            else -> {}
+        }
+    }
+
+    private fun newStateCallback(): StateCallback {
+        return object : StateCallback {
+            override fun onThreadEnableStateChanged(enabledState: Int) {
+                threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED
+                preference?.let { preference -> refreshSummary(preference) }
+            }
+
+            override fun onDeviceRoleChanged(role: Int) {}
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt
deleted file mode 100644
index 10e3f84..0000000
--- a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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.threadnetwork
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageManager
-import android.net.thread.ThreadNetworkController
-import android.net.thread.ThreadNetworkController.StateCallback
-import android.net.thread.ThreadNetworkException
-import android.net.thread.ThreadNetworkManager
-import android.os.OutcomeReceiver
-import android.provider.Settings
-import android.util.Log
-import androidx.annotation.VisibleForTesting
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.preference.Preference
-import androidx.preference.PreferenceScreen
-import com.android.net.thread.platform.flags.Flags
-import com.android.settings.R
-import com.android.settings.core.TogglePreferenceController
-import java.util.concurrent.Executor
-
-/** Controller for the "Thread" toggle in "Connected devices > Connection preferences".  */
-class ThreadNetworkPreferenceController @VisibleForTesting constructor(
-    context: Context,
-    key: String,
-    private val executor: Executor,
-    private val threadController: BaseThreadNetworkController?
-) : TogglePreferenceController(context, key), LifecycleEventObserver {
-    private val stateCallback: StateCallback
-    private val airplaneModeReceiver: BroadcastReceiver
-    private var threadEnabled = false
-    private var airplaneModeOn = false
-    private var preference: Preference? = null
-
-    /**
-     * A testable interface for [ThreadNetworkController] which is `final`.
-     *
-     * We are in a awkward situation that Android API guideline suggest `final` for API classes
-     * while Robolectric test is being deprecated for platform testing (See
-     * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's
-     * conflicting with the default "mockito-target" which is somehow indirectly depended by the
-     * `SettingsUnitTests` target.
-     */
-    @VisibleForTesting
-    interface BaseThreadNetworkController {
-        fun setEnabled(
-            enabled: Boolean,
-            executor: Executor,
-            receiver: OutcomeReceiver<Void?, ThreadNetworkException>
-        )
-
-        fun registerStateCallback(executor: Executor, callback: StateCallback)
-
-        fun unregisterStateCallback(callback: StateCallback)
-    }
-
-    constructor(context: Context, key: String) : this(
-        context,
-        key,
-        ContextCompat.getMainExecutor(context),
-        getThreadNetworkController(context)
-    )
-
-    init {
-        stateCallback = newStateCallback()
-        airplaneModeReceiver = newAirPlaneModeReceiver()
-    }
-
-    val isThreadSupportedOnDevice: Boolean
-        get() = threadController != null
-
-    private fun newStateCallback(): StateCallback {
-        return object : StateCallback {
-            override fun onThreadEnableStateChanged(enabledState: Int) {
-                threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED
-            }
-
-            override fun onDeviceRoleChanged(role: Int) {}
-        }
-    }
-
-    private fun newAirPlaneModeReceiver(): BroadcastReceiver {
-        return object : BroadcastReceiver() {
-            override fun onReceive(context: Context, intent: Intent) {
-                airplaneModeOn = isAirplaneModeOn(context)
-                Log.i(TAG, "Airplane mode is " + if (airplaneModeOn) "ON" else "OFF")
-                preference?.let { preference -> updateState(preference) }
-            }
-        }
-    }
-
-    override fun getAvailabilityStatus(): Int {
-        return if (!Flags.threadEnabledPlatform()) {
-            CONDITIONALLY_UNAVAILABLE
-        } else if (!isThreadSupportedOnDevice) {
-            UNSUPPORTED_ON_DEVICE
-        } else if (airplaneModeOn) {
-            DISABLED_DEPENDENT_SETTING
-        } else {
-            AVAILABLE
-        }
-    }
-
-    override fun displayPreference(screen: PreferenceScreen) {
-        super.displayPreference(screen)
-        preference = screen.findPreference(preferenceKey)
-    }
-
-    override fun isChecked(): Boolean {
-        // TODO (b/322742298):
-        // Check airplane mode here because it's planned to disable Thread state in airplane mode
-        // (code in the mainline module). But it's currently not implemented yet (b/322742298).
-        // By design, the toggle should be unchecked in airplane mode, so explicitly check the
-        // airplane mode here to acchieve the same UX.
-        return !airplaneModeOn && threadEnabled
-    }
-
-    override fun setChecked(isChecked: Boolean): Boolean {
-        if (threadController == null) {
-            return false
-        }
-        val action = if (isChecked) "enable" else "disable"
-        threadController.setEnabled(
-            isChecked,
-            executor,
-            object : OutcomeReceiver<Void?, ThreadNetworkException> {
-                override fun onError(e: ThreadNetworkException) {
-                    // TODO(b/327549838): gracefully handle the failure by resetting the UI state
-                    Log.e(TAG, "Failed to $action Thread", e)
-                }
-
-                override fun onResult(unused: Void?) {
-                    Log.d(TAG, "Successfully $action Thread")
-                }
-            })
-        return true
-    }
-
-    override fun onStateChanged(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event) {
-        if (threadController == null) {
-            return
-        }
-
-        when (event) {
-            Lifecycle.Event.ON_START -> {
-                threadController.registerStateCallback(executor, stateCallback)
-                airplaneModeOn = isAirplaneModeOn(mContext)
-                mContext.registerReceiver(
-                    airplaneModeReceiver,
-                    IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
-                )
-                preference?.let { preference -> updateState(preference) }
-            }
-            Lifecycle.Event.ON_STOP -> {
-                threadController.unregisterStateCallback(stateCallback)
-                mContext.unregisterReceiver(airplaneModeReceiver)
-            }
-            else -> {}
-        }
-    }
-
-    override fun updateState(preference: Preference) {
-        super.updateState(preference)
-        preference.isEnabled = !airplaneModeOn
-        refreshSummary(preference)
-    }
-
-    override fun getSummary(): CharSequence {
-        val resId: Int = if (airplaneModeOn) {
-            R.string.thread_network_settings_summary_airplane_mode
-        } else {
-            R.string.thread_network_settings_summary
-        }
-        return mContext.getResources().getString(resId)
-    }
-
-    override fun getSliceHighlightMenuRes(): Int {
-        return R.string.menu_key_connected_devices
-    }
-
-    companion object {
-        private const val TAG = "ThreadNetworkSettings"
-        private fun getThreadNetworkController(context: Context): BaseThreadNetworkController? {
-            if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) {
-                return null
-            }
-            val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null
-            val controller = manager.allThreadNetworkControllers[0]
-            return object : BaseThreadNetworkController {
-                override fun setEnabled(
-                    enabled: Boolean,
-                    executor: Executor,
-                    receiver: OutcomeReceiver<Void?, ThreadNetworkException>
-                ) {
-                    controller.setEnabled(enabled, executor, receiver)
-                }
-
-                override fun registerStateCallback(executor: Executor, callback: StateCallback) {
-                    controller.registerStateCallback(executor, callback)
-                }
-
-                override fun unregisterStateCallback(callback: StateCallback) {
-                    controller.unregisterStateCallback(callback)
-                }
-            }
-        }
-
-        private fun isAirplaneModeOn(context: Context): Boolean {
-            return Settings.Global.getInt(
-                context.contentResolver,
-                Settings.Global.AIRPLANE_MODE_ON,
-                0
-            ) == 1
-        }
-    }
-}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt
new file mode 100644
index 0000000..2af4675
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.TogglePreferenceController
+import com.android.settings.flags.Flags
+import java.util.concurrent.Executor
+
+/**
+ * Controller for the "Use Thread" toggle in "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkToggleController @VisibleForTesting constructor(
+    context: Context,
+    key: String,
+    private val executor: Executor,
+    private val threadController: BaseThreadNetworkController?
+) : TogglePreferenceController(context, key), LifecycleEventObserver {
+    private val stateCallback: StateCallback
+    private var threadEnabled = false
+    private var preference: Preference? = null
+
+    constructor(context: Context, key: String) : this(
+        context,
+        key,
+        ContextCompat.getMainExecutor(context),
+        ThreadNetworkUtils.getThreadNetworkController(context)
+    )
+
+    init {
+        stateCallback = newStateCallback()
+    }
+
+    val isThreadSupportedOnDevice: Boolean
+        get() = threadController != null
+
+    private fun newStateCallback(): StateCallback {
+        return object : StateCallback {
+            override fun onThreadEnableStateChanged(enabledState: Int) {
+                threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED
+                preference?.let { preference -> updateState(preference) }
+            }
+
+            override fun onDeviceRoleChanged(role: Int) {}
+        }
+    }
+
+    override fun getAvailabilityStatus(): Int {
+        return if (!Flags.threadSettingsEnabled()) {
+            CONDITIONALLY_UNAVAILABLE
+        } else if (!isThreadSupportedOnDevice) {
+            UNSUPPORTED_ON_DEVICE
+        } else {
+            AVAILABLE
+        }
+    }
+
+    override fun displayPreference(screen: PreferenceScreen) {
+        super.displayPreference(screen)
+        preference = screen.findPreference(preferenceKey)
+    }
+
+    override fun isChecked(): Boolean {
+        return threadEnabled
+    }
+
+    override fun setChecked(isChecked: Boolean): Boolean {
+        if (threadController == null) {
+            return false
+        }
+
+        // Avoids dead loop of setChecked -> threadController.setEnabled() ->
+        // StateCallback.onThreadEnableStateChanged -> updateState -> setChecked
+        if (isChecked == isChecked()) {
+            return true
+        }
+
+        val action = if (isChecked) "enable" else "disable"
+        threadController.setEnabled(
+            isChecked,
+            executor,
+            object : OutcomeReceiver<Void?, ThreadNetworkException> {
+                override fun onError(e: ThreadNetworkException) {
+                    // TODO(b/327549838): gracefully handle the failure by resetting the UI state
+                    Log.e(TAG, "Failed to $action Thread", e)
+                }
+
+                override fun onResult(unused: Void?) {
+                    Log.d(TAG, "Successfully $action Thread")
+                }
+            })
+        return true
+    }
+
+    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+        if (threadController == null) {
+            return
+        }
+
+        when (event) {
+            Lifecycle.Event.ON_START -> {
+                threadController.registerStateCallback(executor, stateCallback)
+            }
+
+            Lifecycle.Event.ON_STOP -> {
+                threadController.unregisterStateCallback(stateCallback)
+            }
+
+            else -> {}
+        }
+    }
+
+    override fun getSliceHighlightMenuRes(): Int {
+        return R.string.menu_key_connected_devices
+    }
+
+    companion object {
+        private const val TAG = "ThreadNetworkSettings"
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt
new file mode 100644
index 0000000..70830ed
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import android.net.thread.ThreadNetworkException
+import android.net.thread.ThreadNetworkManager
+import android.os.OutcomeReceiver
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/** Common utilities for Thread settings classes. */
+object ThreadNetworkUtils {
+    /**
+     * Retrieves the [BaseThreadNetworkController] instance that is backed by the Android
+     * [ThreadNetworkController].
+     */
+    fun getThreadNetworkController(context: Context): BaseThreadNetworkController? {
+        if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) {
+            return null
+        }
+        val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null
+        val controller = manager.allThreadNetworkControllers[0]
+        return object : BaseThreadNetworkController {
+            override fun setEnabled(
+                enabled: Boolean,
+                executor: Executor,
+                receiver: OutcomeReceiver<Void?, ThreadNetworkException>
+            ) {
+                controller.setEnabled(enabled, executor, receiver)
+            }
+
+            override fun registerStateCallback(executor: Executor, callback: StateCallback) {
+                controller.registerStateCallback(executor, callback)
+            }
+
+            override fun unregisterStateCallback(callback: StateCallback) {
+                controller.unregisterStateCallback(callback)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/development/BluetoothA2dpConfigStore.java b/src/com/android/settings/development/BluetoothA2dpConfigStore.java
index 7fd7b13..d6b849f 100644
--- a/src/com/android/settings/development/BluetoothA2dpConfigStore.java
+++ b/src/com/android/settings/development/BluetoothA2dpConfigStore.java
@@ -16,15 +16,19 @@
 
 package com.android.settings.development;
 
+import android.annotation.FlaggedApi;
 import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecType;
 
-/**
- * Utility class for storing current Bluetooth A2DP profile values
- */
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Utility class for storing current Bluetooth A2DP profile values */
 public class BluetoothA2dpConfigStore {
 
     // init default values
-    private int mCodecType = BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID;
+    private int mCodecTypeNative = BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID;
+    @Nullable private BluetoothCodecType mCodecType = null;
     private int mCodecPriority = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
     private int mSampleRate = BluetoothCodecConfig.SAMPLE_RATE_NONE;
     private int mBitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_NONE;
@@ -35,6 +39,10 @@
     private long mCodecSpecific4Value;
 
     public void setCodecType(int codecType) {
+        mCodecTypeNative = codecType;
+    }
+
+    public void setCodecType(@Nullable BluetoothCodecType codecType) {
         mCodecType = codecType;
     }
 
@@ -70,9 +78,26 @@
         mCodecSpecific4Value = codecSpecific4Value;
     }
 
+    /** Create codec config utilizing {@link BluetoothCodecConfig.SourceCodecType} */
     public BluetoothCodecConfig createCodecConfig() {
         return new BluetoothCodecConfig.Builder()
-                .setCodecType(mCodecType)
+                .setCodecType(mCodecTypeNative)
+                .setCodecPriority(mCodecPriority)
+                .setSampleRate(mSampleRate)
+                .setBitsPerSample(mBitsPerSample)
+                .setChannelMode(mChannelMode)
+                .setCodecSpecific1(mCodecSpecific1Value)
+                .setCodecSpecific2(mCodecSpecific2Value)
+                .setCodecSpecific3(mCodecSpecific3Value)
+                .setCodecSpecific4(mCodecSpecific4Value)
+                .build();
+    }
+
+    /** Create codec config utilizing {@link BluetoothCodecType} */
+    @FlaggedApi(Flags.FLAG_A2DP_OFFLOAD_CODEC_EXTENSIBILITY_SETTINGS)
+    public @NonNull BluetoothCodecConfig createCodecConfigFromCodecType() {
+        return new BluetoothCodecConfig.Builder()
+                .setExtendedCodecType(mCodecType)
                 .setCodecPriority(mCodecPriority)
                 .setSampleRate(mSampleRate)
                 .setBitsPerSample(mBitsPerSample)
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index cfa4a58..5897965 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -62,10 +62,12 @@
 import com.android.settings.development.autofill.AutofillLoggingLevelPreferenceController;
 import com.android.settings.development.autofill.AutofillResetOptionsPreferenceController;
 import com.android.settings.development.bluetooth.AbstractBluetoothDialogPreferenceController;
+import com.android.settings.development.bluetooth.AbstractBluetoothListPreferenceController;
 import com.android.settings.development.bluetooth.AbstractBluetoothPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothBitPerSampleDialogPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothChannelModeDialogPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothCodecDialogPreferenceController;
+import com.android.settings.development.bluetooth.BluetoothCodecListPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothHDAudioPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothQualityDialogPreferenceController;
 import com.android.settings.development.bluetooth.BluetoothSampleRateDialogPreferenceController;
@@ -381,6 +383,13 @@
                         || enableAngleController.isDefaultValue())) {
                     disableDeveloperOptions();
                 } else {
+                    // Disabling developer options in page-agnostic mode isn't supported as device
+                    // isn't in production state
+                    if (Enable16kUtils.isPageAgnosticModeOn(getContext())) {
+                        Enable16kUtils.showPageAgnosticWarning(getContext());
+                        onDisableDevelopmentOptionsRejected();
+                        return;
+                    }
                     DisableDevSettingsDialogFragment.show(this /* host */);
                 }
             }
@@ -592,6 +601,15 @@
         if (Utils.isMonkeyRunning()) {
             return;
         }
+
+        // Disabling developer options in page-agnostic mode isn't supported as device isn't in
+        // production state
+        if (Enable16kUtils.isPageAgnosticModeOn(getContext())) {
+            Enable16kUtils.showPageAgnosticWarning(getContext());
+            onDisableDevelopmentOptionsRejected();
+            return;
+        }
+
         DevelopmentSettingsEnabler.setDevelopmentSettingsEnabled(getContext(), false);
         final SystemPropPoker poker = SystemPropPoker.getInstance();
         poker.blockPokes();
@@ -742,6 +760,9 @@
         controllers.add(new AutofillResetOptionsPreferenceController(context));
         controllers.add(new BluetoothCodecDialogPreferenceController(context, lifecycle,
                 bluetoothA2dpConfigStore, fragment));
+        controllers.add(
+                new BluetoothCodecListPreferenceController(
+                        context, lifecycle, bluetoothA2dpConfigStore, fragment));
         controllers.add(new BluetoothSampleRateDialogPreferenceController(context, lifecycle,
                 bluetoothA2dpConfigStore));
         controllers.add(new BluetoothBitPerSampleDialogPreferenceController(context, lifecycle,
@@ -790,6 +811,9 @@
                 ((AbstractBluetoothDialogPreferenceController) controller).onHDAudioEnabled(
                         enabled);
             }
+            if (controller instanceof AbstractBluetoothListPreferenceController) {
+                ((AbstractBluetoothListPreferenceController) controller).onHDAudioEnabled(enabled);
+            }
         }
     }
 
diff --git a/src/com/android/settings/development/Enable16KBootReceiver.java b/src/com/android/settings/development/Enable16KBootReceiver.java
new file mode 100644
index 0000000..007a67b
--- /dev/null
+++ b/src/com/android/settings/development/Enable16KBootReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * 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.development;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+public class Enable16KBootReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        String action = intent.getAction();
+        if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+            return;
+        }
+
+        // Do nothing if device is not in page-agnostic mode
+        if (!Enable16kUtils.isPageAgnosticModeOn(context)) {
+            return;
+        }
+
+        // start a service to post persistent notification
+        Intent startNotificationIntent = new Intent(context, PageAgnosticNotificationService.class);
+        context.startServiceAsUser(startNotificationIntent, UserHandle.SYSTEM);
+    }
+}
diff --git a/src/com/android/settings/development/Enable16KOemUnlockDialog.java b/src/com/android/settings/development/Enable16KOemUnlockDialog.java
new file mode 100644
index 0000000..8ddded4
--- /dev/null
+++ b/src/com/android/settings/development/Enable16KOemUnlockDialog.java
@@ -0,0 +1,92 @@
+/*
+ * 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.development;
+
+import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/** Dialog when user interacts 16K pages developer option and device is not OEM unlocked */
+public class Enable16KOemUnlockDialog extends InstrumentedDialogFragment
+        implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+    public static final String TAG = "Enable16KOemUnlockDialog";
+
+    /** This method is used to prompt user to do OEM unlock before using 16k */
+    public static void show(@NonNull Fragment hostFragment) {
+        final FragmentManager manager = hostFragment.getActivity().getSupportFragmentManager();
+        Fragment existingFragment = manager.findFragmentByTag(TAG);
+        if (existingFragment == null) {
+            existingFragment = new Enable16KOemUnlockDialog();
+        }
+
+        if (existingFragment instanceof Enable16KOemUnlockDialog) {
+            existingFragment.setTargetFragment(hostFragment, 0 /* requestCode */);
+            ((Enable16KOemUnlockDialog) existingFragment).show(manager, TAG);
+        }
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_ENABLE_16K_PAGES;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.confirm_oem_unlock_for_16k_title)
+                .setMessage(
+                        Html.fromHtml(
+                                getString(R.string.confirm_oem_unlock_for_16k_text),
+                                FROM_HTML_MODE_COMPACT))
+                .setPositiveButton(android.R.string.ok, this /* onClickListener */)
+                .create();
+    }
+
+    @Override
+    public void onClick(@NonNull DialogInterface dialog, int buttonId) {
+        // Do nothing. OEM unlock has to be done by user
+    }
+
+    @Override
+    public void onDismiss(@NonNull DialogInterface dialog) {
+        super.onDismiss(dialog);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        ((TextView) getDialog().findViewById(android.R.id.message))
+                .setMovementMethod(LinkMovementMethod.getInstance());
+    }
+}
diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
index 3f9da57..0572b1b 100644
--- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java
+++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
@@ -22,7 +22,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.PowerManager;
-import android.os.SystemProperties;
+import android.os.RecoverySystem;
 import android.os.SystemUpdateManager;
 import android.os.UpdateEngine;
 import android.os.UpdateEngineStable;
@@ -34,7 +34,6 @@
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
@@ -68,26 +67,26 @@
 public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferenceController
         implements Preference.OnPreferenceChangeListener,
                 PreferenceControllerMixin,
-                Enable16kbPagesDialogHost {
+                Enable16kbPagesDialogHost,
+                EnableExt4DialogHost {
 
     private static final String TAG = "Enable16kPages";
     private static final String REBOOT_REASON = "toggle16k";
     private static final String ENABLE_16K_PAGES = "enable_16k_pages";
-
-    @VisibleForTesting
-    static final String DEV_OPTION_PROPERTY = "ro.product.build.16k_page.enabled";
-
     private static final int ENABLE_4K_PAGE_SIZE = 0;
     private static final int ENABLE_16K_PAGE_SIZE = 1;
 
-    private static final String OTA_16K_PATH = "/system/boot_otas/boot_ota_16k.zip";
-    private static final String OTA_4K_PATH = "/system/boot_otas/boot_ota_4k.zip";
+    private static final String SYSTEM_PATH = "/system";
+    private static final String VENDOR_PATH = "/vendor";
+    private static final String OTA_16K_PATH = "/boot_otas/boot_ota_16k.zip";
+    private static final String OTA_4K_PATH = "/boot_otas/boot_ota_4k.zip";
+
     private static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
     private static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
     private static final int OFFSET_TO_FILE_NAME = 30;
     public static final String EXPERIMENTAL_UPDATE_TITLE = "Android 16K Kernel Experimental Update";
 
-    private @Nullable DevelopmentSettingsDashboardFragment mFragment = null;
+    private @NonNull DevelopmentSettingsDashboardFragment mFragment;
     private boolean mEnable16k;
 
     private final ListeningExecutorService mExecutorService =
@@ -96,14 +95,15 @@
     private AlertDialog mProgressDialog;
 
     public Enable16kPagesPreferenceController(
-            @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) {
+            @NonNull Context context, @NonNull DevelopmentSettingsDashboardFragment fragment) {
         super(context);
-        mFragment = fragment;
+        this.mFragment = fragment;
+        mEnable16k = Enable16kUtils.isUsing16kbPages();
     }
 
     @Override
     public boolean isAvailable() {
-        return SystemProperties.getBoolean(DEV_OPTION_PROPERTY, false);
+        return Enable16kUtils.is16KbToggleAvailable();
     }
 
     @Override
@@ -114,17 +114,29 @@
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         mEnable16k = (Boolean) newValue;
+        // Prompt user to do oem unlock first
+        if (!Enable16kUtils.isDeviceOEMUnlocked(mContext)) {
+            Enable16KOemUnlockDialog.show(mFragment);
+            return false;
+        }
+
+        if (!Enable16kUtils.isDataExt4()) {
+            EnableExt4WarningDialog.show(mFragment, this);
+            return false;
+        }
         Enable16kPagesWarningDialog.show(mFragment, this, mEnable16k);
         return true;
     }
 
     @Override
     public void updateState(Preference preference) {
+        int defaultOptionValue =
+                Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
         final int optionValue =
                 Settings.Global.getInt(
                         mContext.getContentResolver(),
                         Settings.Global.ENABLE_16K_PAGES,
-                        ENABLE_4K_PAGE_SIZE /* default */);
+                        defaultOptionValue /* default */);
 
         ((SwitchPreference) mPreference).setChecked(optionValue == ENABLE_16K_PAGE_SIZE);
     }
@@ -140,6 +152,14 @@
         ((SwitchPreference) mPreference).setChecked(false);
     }
 
+    @Override
+    protected void onDeveloperOptionsSwitchEnabled() {
+        int currentStatus =
+                Enable16kUtils.isUsing16kbPages() ? ENABLE_16K_PAGE_SIZE : ENABLE_4K_PAGE_SIZE;
+        Settings.Global.putInt(
+                mContext.getContentResolver(), Settings.Global.ENABLE_16K_PAGES, currentStatus);
+    }
+
     /** Called when user confirms reboot dialog */
     @Override
     public void on16kPagesDialogConfirmed() {
@@ -162,9 +182,9 @@
                     }
 
                     @Override
-                    public void onFailure(Throwable t) {
+                    public void onFailure(@NonNull Throwable t) {
                         hideProgressDialog();
-                        Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!");
+                        Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!", t);
                         displayToast(mContext.getString(R.string.toast_16k_update_failed_text));
                     }
                 },
@@ -173,7 +193,12 @@
 
     /** Called when user dismisses to reboot dialog */
     @Override
-    public void on16kPagesDialogDismissed() {}
+    public void on16kPagesDialogDismissed() {
+        if (mPreference == null) {
+            return;
+        }
+        updateState(mPreference);
+    }
 
     private void installUpdate() {
         // Check if there is any pending system update
@@ -182,16 +207,19 @@
         int status = data.getInt(SystemUpdateManager.KEY_STATUS);
         if (status != SystemUpdateManager.STATUS_UNKNOWN
                 && status != SystemUpdateManager.STATUS_IDLE) {
-            throw new RuntimeException("System has pending update!");
+            throw new RuntimeException(
+                    "System has pending update! Please restart the device to complete applying"
+                            + " pending update. If you are seeing this after using 16KB developer"
+                            + " options, please check configuration and OTA packages!");
         }
 
         // Publish system update info
         PersistableBundle info = createUpdateInfo(SystemUpdateManager.STATUS_IN_PROGRESS);
         manager.updateSystemUpdateInfo(info);
 
-        String updateFilePath = mEnable16k ? OTA_16K_PATH : OTA_4K_PATH;
         try {
-            File updateFile = new File(updateFilePath);
+            File updateFile = getOtaFile();
+            Log.i(TAG, "Update file path is " + updateFile.getAbsolutePath());
             applyUpdateFile(updateFile);
         } catch (IOException e) {
             throw new RuntimeException(e);
@@ -288,7 +316,42 @@
     }
 
     private void displayToast(String message) {
-        Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+        Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+    }
+
+    @Override
+    public void onExt4DialogConfirmed() {
+        // user has confirmed to wipe the device
+        ListenableFuture future = mExecutorService.submit(() -> wipeData());
+        Futures.addCallback(
+                future,
+                new FutureCallback<>() {
+                    @Override
+                    public void onSuccess(@NonNull Object result) {
+                        Log.i(TAG, "Wiping /data  with recovery system.");
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.e(TAG, "Failed to change the /data partition to ext4");
+                        displayToast(mContext.getString(R.string.format_ext4_failure_toast));
+                    }
+                },
+                ContextCompat.getMainExecutor(mContext));
+    }
+
+    private void wipeData() {
+        RecoverySystem recoveryService = mContext.getSystemService(RecoverySystem.class);
+        try {
+            recoveryService.wipePartitionToExt4();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void onExt4DialogDismissed() {
+        // Do nothing
     }
 
     private class OtaUpdateCallback extends UpdateEngineStableCallback {
@@ -345,6 +408,7 @@
                         LinearLayout.LayoutParams.WRAP_CONTENT,
                         LinearLayout.LayoutParams.WRAP_CONTENT);
         progressBar.setLayoutParams(params);
+        progressBar.setPadding(0, 24, 0, 24);
         builder.setView(progressBar);
         builder.setCancelable(false);
         return builder.create();
@@ -357,4 +421,23 @@
         infoBundle.putString(SystemUpdateManager.KEY_TITLE, EXPERIMENTAL_UPDATE_TITLE);
         return infoBundle;
     }
+
+    // if BOARD_16K_OTA_MOVE_VENDOR, OTAs will be present on the /vendor partition
+    private File getOtaFile() throws FileNotFoundException {
+        String otaPath = mEnable16k ? OTA_16K_PATH : OTA_4K_PATH;
+        // Check if boot ota exists on vendor path and prefer vendor ota if present
+        String vendorOta = VENDOR_PATH + otaPath;
+        File vendorOtaFile = new File(vendorOta);
+        if (vendorOtaFile != null && vendorOtaFile.exists()) {
+            return vendorOtaFile;
+        }
+
+        // otherwise, fallback to boot ota from system partition
+        String systemOta = SYSTEM_PATH + otaPath;
+        File systemOtaFile = new File(systemOta);
+        if (systemOtaFile == null || !systemOtaFile.exists()) {
+            throw new FileNotFoundException("File not found at path " + systemOta);
+        }
+        return systemOtaFile;
+    }
 }
diff --git a/src/com/android/settings/development/Enable16kUtils.java b/src/com/android/settings/development/Enable16kUtils.java
new file mode 100644
index 0000000..00b7ee9
--- /dev/null
+++ b/src/com/android/settings/development/Enable16kUtils.java
@@ -0,0 +1,129 @@
+/*
+ * 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.development;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.oemlock.OemLockManager;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class Enable16kUtils {
+    private static final long PAGE_SIZE = Os.sysconf(OsConstants._SC_PAGESIZE);
+    private static final int PAGE_SIZE_16KB = 16 * 1024;
+
+    @VisibleForTesting
+    static final String DEV_OPTION_PROPERTY = "ro.product.build.16k_page.enabled";
+
+    private static final String TAG = "Enable16kUtils";
+
+    /**
+     * @param context uses context to retrieve OEM unlock info
+     * @return true if device is OEM unlocked and factory reset is allowed for user.
+     */
+    public static boolean isDeviceOEMUnlocked(@NonNull Context context) {
+        // OEM unlock is checked for bootloader, carrier and user. Check all three to ensure
+        // that device is unlocked and it is also allowed by user as well as carrier
+        final OemLockManager oemLockManager = context.getSystemService(OemLockManager.class);
+        final UserManager userManager = context.getSystemService(UserManager.class);
+        if (oemLockManager == null || userManager == null) {
+            Log.e(TAG, "Required services not found on device to check for OEM unlock state.");
+            return false;
+        }
+
+        // If either of device or carrier is not allowed to unlock, return false
+        if (!oemLockManager.isDeviceOemUnlocked()) {
+            Log.e(TAG, "Device is not OEM unlocked");
+            return false;
+        }
+
+        final UserHandle userHandle = UserHandle.of(UserHandle.myUserId());
+        if (userManager.hasBaseUserRestriction(UserManager.DISALLOW_FACTORY_RESET, userHandle)) {
+            Log.e(TAG, "Factory reset is not allowed for user.");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @return true if /data partition is ext4
+     */
+    public static boolean isDataExt4() {
+        try (BufferedReader br = new BufferedReader(new FileReader("/proc/mounts"))) {
+            String line;
+            while ((line = br.readLine()) != null) {
+                Log.i(TAG, line);
+                final String[] fields = line.split(" ");
+                final String partition = fields[1];
+                final String fsType = fields[2];
+                if (partition.equals("/data") && fsType.equals("ext4")) {
+                    return true;
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to read /proc/mounts");
+        }
+
+        return false;
+    }
+
+    /**
+     * @return returns true if 16KB developer option is available for the device.
+     */
+    public static boolean is16KbToggleAvailable() {
+        return SystemProperties.getBoolean(DEV_OPTION_PROPERTY, false);
+    }
+
+    /**
+     * 16kB page-agnostic mode requires /data to be ext4, ro.product.build.16k_page.enabled for
+     * device and Device OEM unlocked.
+     *
+     * @param context is needed to query OEM unlock state
+     * @return true if device is in page-agnostic mode.
+     */
+    public static boolean isPageAgnosticModeOn(@NonNull Context context) {
+        return is16KbToggleAvailable() && isDeviceOEMUnlocked(context) && isDataExt4();
+    }
+
+    /**
+     * @return returns true if current page size is 16KB
+     */
+    public static boolean isUsing16kbPages() {
+        return PAGE_SIZE == PAGE_SIZE_16KB;
+    }
+
+    /**
+     * show page-agnostic mode warning dialog to user
+     * @param context to start activity
+     */
+    public static void showPageAgnosticWarning(@NonNull Context context) {
+        Intent intent = new Intent(context, PageAgnosticWarningActivity.class);
+        context.startActivityAsUser(intent, UserHandle.SYSTEM);
+    }
+}
diff --git a/src/com/android/settings/development/EnableExt4DialogHost.java b/src/com/android/settings/development/EnableExt4DialogHost.java
new file mode 100644
index 0000000..6cbd7e1
--- /dev/null
+++ b/src/com/android/settings/development/EnableExt4DialogHost.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.development;
+
+/** Interface for EnableExt4DialogHost callbacks. */
+public interface EnableExt4DialogHost {
+    /** Callback when the user presses ok the warning dialog. */
+    void onExt4DialogConfirmed();
+
+    /** Callback when the user cancels or dismisses the warning dialog. */
+    void onExt4DialogDismissed();
+}
diff --git a/src/com/android/settings/development/EnableExt4WarningDialog.java b/src/com/android/settings/development/EnableExt4WarningDialog.java
new file mode 100644
index 0000000..295d64b
--- /dev/null
+++ b/src/com/android/settings/development/EnableExt4WarningDialog.java
@@ -0,0 +1,94 @@
+/*
+ * 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.development;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.internal.annotations.Initializer;
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/** Dialog when user interacts 16K pages developer option and data is f2fs */
+public class EnableExt4WarningDialog extends InstrumentedDialogFragment
+        implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+    public static final String TAG = "EnableExt4WarningDialog";
+
+    private EnableExt4DialogHost mHost;
+
+    @Initializer
+    private void setHost(@NonNull EnableExt4DialogHost host) {
+        this.mHost = host;
+    }
+
+    /** This method is used to show warning dialog to reformat data to /ext4 */
+    public static void show(
+            @NonNull Fragment hostFragment, @NonNull EnableExt4DialogHost dialogHost) {
+        final FragmentManager manager = hostFragment.getActivity().getSupportFragmentManager();
+        Fragment existingFragment = manager.findFragmentByTag(TAG);
+        if (existingFragment == null) {
+            existingFragment = new EnableExt4WarningDialog();
+        }
+
+        if (existingFragment instanceof EnableExt4WarningDialog) {
+            existingFragment.setTargetFragment(hostFragment, 0 /* requestCode */);
+            ((EnableExt4WarningDialog) existingFragment).setHost(dialogHost);
+            ((EnableExt4WarningDialog) existingFragment).show(manager, TAG);
+        }
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_ENABLE_16K_PAGES;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.confirm_format_ext4_title)
+                .setIcon(R.drawable.ic_delete_accent)
+                .setMessage(R.string.confirm_format_ext4_text)
+                .setPositiveButton(R.string.confirm_ext4_button_text, this /* onClickListener */)
+                .setNegativeButton(android.R.string.cancel, this /* onClickListener */)
+                .create();
+    }
+
+    @Override
+    public void onClick(@NonNull DialogInterface dialog, int buttonId) {
+        if (buttonId == DialogInterface.BUTTON_POSITIVE) {
+            mHost.onExt4DialogConfirmed();
+        } else {
+            mHost.onExt4DialogDismissed();
+        }
+    }
+
+    @Override
+    public void onDismiss(@NonNull DialogInterface dialog) {
+        super.onDismiss(dialog);
+        mHost.onExt4DialogDismissed();
+    }
+}
diff --git a/src/com/android/settings/development/PageAgnosticNotificationService.java b/src/com/android/settings/development/PageAgnosticNotificationService.java
new file mode 100644
index 0000000..bce1dd9
--- /dev/null
+++ b/src/com/android/settings/development/PageAgnosticNotificationService.java
@@ -0,0 +1,139 @@
+/*
+ * 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.development;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+public class PageAgnosticNotificationService extends Service {
+
+    private static final String NOTIFICATION_CHANNEL_ID =
+            "com.android.settings.development.PageAgnosticNotificationService";
+    private static final int NOTIFICATION_ID = 1;
+
+    static final int DISABLE_UPDATES_SETTING = 1;
+
+    private NotificationManager mNotificationManager;
+
+    @Nullable
+    @Override
+    public IBinder onBind(@NonNull Intent intent) {
+        return null;
+    }
+
+    // create a notification channel to post persistent notification
+    private void createNotificationChannel() {
+        NotificationChannel channel =
+                new NotificationChannel(
+                        NOTIFICATION_CHANNEL_ID,
+                        getString(R.string.page_agnostic_notification_channel_name),
+                        NotificationManager.IMPORTANCE_HIGH);
+        mNotificationManager = getSystemService(NotificationManager.class);
+        if (mNotificationManager != null) {
+            mNotificationManager.createNotificationChannel(channel);
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        createNotificationChannel();
+    }
+
+    private Notification buildNotification() {
+        // Get the title and text according to page size
+        boolean isIn16kbMode = Enable16kUtils.isUsing16kbPages();
+        String title =
+                isIn16kbMode
+                        ? getString(R.string.page_agnostic_16k_pages_title)
+                        : getString(R.string.page_agnostic_4k_pages_title);
+        String text =
+                isIn16kbMode
+                        ? getString(R.string.page_agnostic_16k_pages_text_short)
+                        : getString(R.string.page_agnostic_4k_pages_text_short);
+
+        Intent notifyIntent = new Intent(this, PageAgnosticWarningActivity.class);
+        // Set the Activity to start in a new, empty task.
+        notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        // Create the PendingIntent.
+        PendingIntent notifyPendingIntent =
+                PendingIntent.getActivity(
+                        this,
+                        0,
+                        notifyIntent,
+                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Notification.Action action =
+                new Notification.Action.Builder(
+                                R.drawable.empty_icon,
+                                getString(R.string.page_agnostic_notification_action),
+                                notifyPendingIntent)
+                        .build();
+
+        Notification.Builder builder =
+                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(title)
+                        .setContentText(text)
+                        .setOngoing(true)
+                        .setSmallIcon(R.drawable.ic_settings_24dp)
+                        .setStyle(new Notification.BigTextStyle().bigText(text))
+                        .setContentIntent(notifyPendingIntent)
+                        .addAction(action);
+
+        return builder.build();
+    }
+
+    private void disableAutomaticUpdates() {
+        final int currentState =
+                Settings.Global.getInt(
+                        getApplicationContext().getContentResolver(),
+                        Settings.Global.OTA_DISABLE_AUTOMATIC_UPDATE,
+                        0 /* default */);
+        // 0 means enabled, 1 means disabled
+        if (currentState == 0) {
+            // automatic updates are enabled, disable them
+            Settings.Global.putInt(
+                    getApplicationContext().getContentResolver(),
+                    Settings.Global.OTA_DISABLE_AUTOMATIC_UPDATE,
+                    DISABLE_UPDATES_SETTING);
+        }
+    }
+
+    @Override
+    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+        Notification notification = buildNotification();
+        if (mNotificationManager != null) {
+            mNotificationManager.notify(NOTIFICATION_ID, notification);
+        }
+
+        // No updates should be allowed in page-agnostic mode
+        disableAutomaticUpdates();
+        return Service.START_NOT_STICKY;
+    }
+}
diff --git a/src/com/android/settings/development/PageAgnosticWarningActivity.java b/src/com/android/settings/development/PageAgnosticWarningActivity.java
new file mode 100644
index 0000000..8fd6074
--- /dev/null
+++ b/src/com/android/settings/development/PageAgnosticWarningActivity.java
@@ -0,0 +1,72 @@
+/*
+ * 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.development;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.method.LinkMovementMethod;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+
+public class PageAgnosticWarningActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        String title =
+                Enable16kUtils.isUsing16kbPages()
+                        ? getString(R.string.page_agnostic_16k_pages_title)
+                        : getString(R.string.page_agnostic_4k_pages_title);
+
+        String warningText =
+                Enable16kUtils.isUsing16kbPages()
+                        ? getString(R.string.page_agnostic_16k_pages_text)
+                        : getString(R.string.page_agnostic_4k_pages_text);
+        showWarningDialog(title, warningText);
+    }
+
+    // Create warning dialog and make links clickable
+    private void showWarningDialog(String title, String warningText) {
+
+        AlertDialog dialog =
+                new AlertDialog.Builder(this)
+                        .setTitle(title)
+                        .setMessage(Html.fromHtml(warningText, Html.FROM_HTML_MODE_COMPACT))
+                        .setCancelable(false)
+                        .setPositiveButton(
+                                android.R.string.ok,
+                                new DialogInterface.OnClickListener() {
+                                    public void onClick(
+                                            @NonNull DialogInterface dialog, int which) {
+                                        dialog.cancel();
+                                        finish();
+                                    }
+                                })
+                        .create();
+        dialog.show();
+
+        ((TextView) dialog.findViewById(android.R.id.message))
+                .setMovementMethod(LinkMovementMethod.getInstance());
+    }
+}
diff --git a/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceController.java b/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceController.java
new file mode 100644
index 0000000..9436e06
--- /dev/null
+++ b/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceController.java
@@ -0,0 +1,268 @@
+/*
+ * 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.development.bluetooth;
+
+import static android.bluetooth.BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.development.BluetoothA2dpConfigStore;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.List;
+
+/** Abstract class for Bluetooth A2DP config list controller in developer option. */
+public abstract class AbstractBluetoothListPreferenceController
+        extends AbstractBluetoothPreferenceController
+        implements Preference.OnPreferenceChangeListener {
+
+    private static final String TAG = "AbstrBtListPrefCtrl";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    protected static final int DEFAULT_VALUE_INT = 1000;
+
+    @Nullable protected ListPreference mListPreference;
+
+    protected String mDefaultEntry;
+    protected String mDefaultValue;
+
+    @Nullable protected final BluetoothA2dpConfigStore mBluetoothA2dpConfigStore;
+
+    public AbstractBluetoothListPreferenceController(
+            @NonNull Context context,
+            @Nullable Lifecycle lifecycle,
+            @Nullable BluetoothA2dpConfigStore store) {
+        super(context, lifecycle, store);
+
+        mDefaultEntry = mContext.getString(R.string.bluetooth_audio_codec_default_selection);
+        mDefaultValue = String.valueOf(DEFAULT_VALUE_INT);
+
+        mBluetoothA2dpConfigStore = store;
+    }
+
+    @Override
+    public void displayPreference(@NonNull PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mListPreference = screen.findPreference(getPreferenceKey());
+    }
+
+    @Override
+    public boolean onPreferenceChange(@Nullable Preference preference, @NonNull Object newValue) {
+        if (DEBUG) {
+            Log.d(TAG, "onPreferenceChange: newValue=" + (String) newValue);
+        }
+        if (mListPreference == null) {
+            Log.e(TAG, "onPreferenceChange: List preference is null");
+            return false;
+        }
+        updateState(mListPreference);
+        return true;
+    }
+
+    @Override
+    public void updateState(@Nullable Preference preference) {
+        setupDefaultListPreference();
+    }
+
+    @Override
+    public void onBluetoothServiceConnected(@NonNull BluetoothA2dp bluetoothA2dp) {
+        super.onBluetoothServiceConnected(bluetoothA2dp);
+        initConfigStore();
+    }
+
+    @Override
+    protected void onDeveloperOptionsSwitchDisabled() {
+        super.onDeveloperOptionsSwitchDisabled();
+        if (DEBUG) {
+            Log.d(TAG, "onDeveloperOptionsSwitchDisabled");
+        }
+        if (mListPreference == null) {
+            Log.e(TAG, "onDeveloperOptionsSwitchDisabled: List preference is null");
+            return;
+        }
+        updateState(mListPreference);
+    }
+
+    /**
+     * Method to notify controller when the HD audio(optional codec) state is changed.
+     *
+     * @param enabled Is {@code true} when the setting is enabled.
+     */
+    public void onHDAudioEnabled(boolean enabled) {}
+
+    /**
+     * Updates the new value to the {@link BluetoothA2dpConfigStore}.
+     *
+     * @param entryValue the new setting entry value
+     */
+    protected abstract void writeConfigurationValues(String entryValue);
+
+    /**
+     * Gets the current bluetooth codec status.
+     *
+     * @return {@link BluetoothCodecStatus}.
+     */
+    @Nullable
+    protected BluetoothCodecStatus getBluetoothCodecStatus() {
+        final BluetoothA2dp bluetoothA2dp = mBluetoothA2dp;
+        if (bluetoothA2dp == null) {
+            Log.e(
+                    TAG,
+                    "getBluetoothCodecStatus: Unable to get codec status. Bluetooth A2dp is null.");
+            return null;
+        }
+        final BluetoothDevice activeDevice = getA2dpActiveDevice();
+        if (activeDevice == null) {
+            Log.e(TAG, "getBluetoothCodecStatus: Unable to get codec status. No active device.");
+            return null;
+        }
+        final BluetoothCodecStatus codecStatus = bluetoothA2dp.getCodecStatus(activeDevice);
+        if (codecStatus == null) {
+            Log.e(TAG, "getBluetoothCodecStatus: Codec status is null");
+            return null;
+        }
+        return codecStatus;
+    }
+
+    /**
+     * Gets the current bluetooth codec config.
+     *
+     * @return {@link BluetoothCodecConfig}.
+     */
+    @Nullable
+    protected BluetoothCodecConfig getCurrentCodecConfig() {
+        final BluetoothCodecStatus codecStatus = getBluetoothCodecStatus();
+        if (codecStatus == null) {
+            Log.e(
+                    TAG,
+                    "getCurrentCodecConfig: Unable to get current codec config. Codec status is"
+                            + " null");
+            return null;
+        }
+
+        return codecStatus.getCodecConfig();
+    }
+
+    /**
+     * Sets the {@link ListPreference}. This method adds the default entry and the entry value
+     * automatically.
+     *
+     * @param entries list of String entries for the {@link ListPreference}.
+     * @param entryValues list of String entry values for the {@link ListPreference}.
+     * @param selectedEntry currently selected entry.
+     * @param selectedValue currently selected entry value.
+     */
+    protected void setupListPreference(
+            List<String> entries,
+            List<String> entryValues,
+            String selectedEntry,
+            String selectedValue) {
+        if (entries.size() != entryValues.size()) {
+            Log.e(
+                    TAG,
+                    ("setupListPreference: size of entries: " + entries.size())
+                            + (", size of entryValues" + entryValues.size()));
+            setupDefaultListPreference();
+            return;
+        }
+        if (entries.isEmpty() || entryValues.isEmpty()) {
+            Log.e(TAG, "setupListPreference: entries or entryValues empty");
+            setupDefaultListPreference();
+            return;
+        }
+        entries.add(0, mDefaultEntry);
+        entryValues.add(0, mDefaultValue);
+
+        if (mListPreference == null) {
+            Log.e(TAG, "setupListPreference: List preference is null");
+            return;
+        }
+        mListPreference.setEntries(entries.toArray(new String[entries.size()]));
+        mListPreference.setEntryValues(entryValues.toArray(new String[entryValues.size()]));
+        mListPreference.setValue(selectedValue);
+        mListPreference.setSummary(selectedEntry);
+    }
+
+    /**
+     * Check HD Audio enabled.
+     *
+     * @return true if HD Audio is enabled.
+     */
+    protected boolean isHDAudioEnabled() {
+        final BluetoothA2dp bluetoothA2dp = mBluetoothA2dp;
+        if (bluetoothA2dp == null) {
+            Log.e(TAG, "isHDAudioEnabled: Unable to get codec status. BluetoothA2dp is null.");
+            return false;
+        }
+        BluetoothDevice activeDevice = getA2dpActiveDevice();
+        if (activeDevice == null) {
+            Log.e(TAG, "isHDAudioEnabled: Unable to get codec status. No active device.");
+            return false;
+        }
+        return (bluetoothA2dp.isOptionalCodecsEnabled(activeDevice)
+                == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+    }
+
+    private void setupDefaultListPreference() {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "setupDefaultListPreference: mDefaultEntry="
+                            + mDefaultEntry
+                            + ", mDefaultValue="
+                            + mDefaultValue);
+        }
+        if (mListPreference == null) {
+            Log.e(TAG, "setupListPreference: List preference is null");
+            return;
+        }
+        mListPreference.setEntries(new String[] {mDefaultEntry});
+        mListPreference.setEntryValues(new String[] {mDefaultValue});
+        mListPreference.setValue(mDefaultValue);
+        mListPreference.setSummary(mDefaultEntry);
+    }
+
+    private void initConfigStore() {
+        final BluetoothCodecConfig config = getCurrentCodecConfig();
+        if (config == null) {
+            Log.e(TAG, "initConfigStore: Current codec config is null.");
+            return;
+        }
+        if (mBluetoothA2dpConfigStore == null) {
+            Log.e(TAG, "initConfigStore: Bluetooth A2dp Config Store is null.");
+            return;
+        }
+        mBluetoothA2dpConfigStore.setCodecType(config.getExtendedCodecType());
+        mBluetoothA2dpConfigStore.setSampleRate(config.getSampleRate());
+        mBluetoothA2dpConfigStore.setBitsPerSample(config.getBitsPerSample());
+        mBluetoothA2dpConfigStore.setChannelMode(config.getChannelMode());
+        mBluetoothA2dpConfigStore.setCodecPriority(CODEC_PRIORITY_HIGHEST);
+        mBluetoothA2dpConfigStore.setCodecSpecific1Value(config.getCodecSpecific1());
+    }
+}
diff --git a/src/com/android/settings/development/bluetooth/AbstractBluetoothPreferenceController.java b/src/com/android/settings/development/bluetooth/AbstractBluetoothPreferenceController.java
index d3fab67..14bc275 100644
--- a/src/com/android/settings/development/bluetooth/AbstractBluetoothPreferenceController.java
+++ b/src/com/android/settings/development/bluetooth/AbstractBluetoothPreferenceController.java
@@ -23,6 +23,7 @@
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settings.core.PreferenceControllerMixin;
@@ -42,13 +43,15 @@
         DeveloperOptionsPreferenceController implements BluetoothServiceConnectionListener,
         LifecycleObserver, OnDestroy, PreferenceControllerMixin {
 
-    protected volatile BluetoothA2dp mBluetoothA2dp;
+    @Nullable protected volatile BluetoothA2dp mBluetoothA2dp;
 
     @VisibleForTesting
     BluetoothAdapter mBluetoothAdapter;
 
-    public AbstractBluetoothPreferenceController(Context context, Lifecycle lifecycle,
-                                                 BluetoothA2dpConfigStore store) {
+    public AbstractBluetoothPreferenceController(
+            @Nullable Context context,
+            @Nullable Lifecycle lifecycle,
+            @Nullable BluetoothA2dpConfigStore store) {
         super(context);
         if (lifecycle != null) {
             lifecycle.addObserver(this);
diff --git a/src/com/android/settings/development/bluetooth/BluetoothCodecDialogPreferenceController.java b/src/com/android/settings/development/bluetooth/BluetoothCodecDialogPreferenceController.java
index 2f0d27c..b7b5574 100644
--- a/src/com/android/settings/development/bluetooth/BluetoothCodecDialogPreferenceController.java
+++ b/src/com/android/settings/development/bluetooth/BluetoothCodecDialogPreferenceController.java
@@ -26,6 +26,7 @@
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.development.BluetoothA2dpConfigStore;
+import com.android.settings.development.Flags;
 import com.android.settingslib.core.lifecycle.Lifecycle;
 
 import java.util.ArrayList;
@@ -50,6 +51,11 @@
     }
 
     @Override
+    public boolean isAvailable() {
+        return !Flags.a2dpOffloadCodecExtensibilitySettings();
+    }
+
+    @Override
     public String getPreferenceKey() {
         return KEY;
     }
diff --git a/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceController.java b/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceController.java
new file mode 100644
index 0000000..863cd27
--- /dev/null
+++ b/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceController.java
@@ -0,0 +1,273 @@
+/*
+ * 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.development.bluetooth;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothCodecType;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.development.BluetoothA2dpConfigStore;
+import com.android.settings.development.Flags;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** List preference controller to set the Bluetooth A2DP codec */
+public class BluetoothCodecListPreferenceController
+        extends AbstractBluetoothListPreferenceController {
+
+    private static final String KEY = "bluetooth_audio_codec_settings_list";
+    private static final String TAG = "BtExtCodecCtr";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    @Nullable private final Callback mCallback;
+
+    public BluetoothCodecListPreferenceController(
+            @NonNull Context context,
+            @Nullable Lifecycle lifecycle,
+            @Nullable BluetoothA2dpConfigStore store,
+            @Nullable Callback callback) {
+        super(context, lifecycle, store);
+        mCallback = callback;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        boolean available = Flags.a2dpOffloadCodecExtensibilitySettings();
+        if (DEBUG) {
+            Log.d(TAG, "isAvailable: " + available);
+        }
+        return available;
+    }
+
+    @Override
+    public @NonNull String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public void displayPreference(@NonNull PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mListPreference = screen.findPreference(getPreferenceKey());
+    }
+
+    @Override
+    public boolean onPreferenceChange(@Nullable Preference preference, @NonNull Object newValue) {
+        if (!Flags.a2dpOffloadCodecExtensibilitySettings()) {
+            return false;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "onPreferenceChange: newValue=" + (String) newValue);
+        }
+        final BluetoothA2dp bluetoothA2dp = mBluetoothA2dp;
+        if (bluetoothA2dp == null) {
+            Log.e(TAG, "onPreferenceChange: bluetoothA2dp is null");
+            return false;
+        }
+
+        writeConfigurationValues((String) newValue);
+
+        if (mBluetoothA2dpConfigStore == null) {
+            Log.e(TAG, "onPreferenceChange: Bluetooth A2dp Config Store is null");
+            return false;
+        }
+        BluetoothCodecConfig codecConfig;
+        if (Flags.a2dpOffloadCodecExtensibilitySettings()) {
+            codecConfig = mBluetoothA2dpConfigStore.createCodecConfigFromCodecType();
+        } else {
+            codecConfig = mBluetoothA2dpConfigStore.createCodecConfig();
+        }
+
+        final BluetoothDevice activeDevice = getA2dpActiveDevice();
+        if (activeDevice == null) {
+            Log.e(TAG, "onPreferenceChange: active device is null");
+            return false;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "onPreferenceChange: setCodecConfigPreference: " + codecConfig.toString());
+        }
+        bluetoothA2dp.setCodecConfigPreference(activeDevice, codecConfig);
+        if (mCallback != null) {
+            mCallback.onBluetoothCodecChanged();
+        }
+
+        return true;
+    }
+
+    @Override
+    public void updateState(@Nullable Preference preference) {
+        super.updateState(preference);
+        if (!Flags.a2dpOffloadCodecExtensibilitySettings()) {
+            return;
+        }
+
+        final List<String> codecIds = new ArrayList<>();
+        final List<String> labels = new ArrayList<>();
+        String selectedCodecId = mDefaultValue;
+        String selectedLabel = mDefaultEntry;
+
+        if (isHDAudioEnabled()) {
+            final BluetoothCodecStatus codecStatus = getBluetoothCodecStatus();
+            if (codecStatus == null) {
+                Log.e(TAG, "updateState: Bluetooth Codec Status is null");
+                return;
+            }
+
+            final BluetoothCodecConfig currentCodecConfig = codecStatus.getCodecConfig();
+            if (currentCodecConfig == null) {
+                Log.e(TAG, "updateState: currentCodecConfig is null");
+                return;
+            }
+
+            final BluetoothA2dp bluetoothA2dp = mBluetoothA2dp;
+            if (bluetoothA2dp == null) {
+                Log.e(TAG, "updateState: bluetoothA2dp is null");
+                return;
+            }
+
+            final Collection<BluetoothCodecType> codecTypes =
+                    bluetoothA2dp.getSupportedCodecTypes();
+            for (BluetoothCodecType codecType : codecTypes) {
+                labels.add(codecType.getCodecName());
+                codecIds.add(String.valueOf(codecType.getCodecId()));
+                if (currentCodecConfig != null
+                        && currentCodecConfig.getExtendedCodecType().equals(codecType)) {
+                    selectedCodecId = codecIds.get(codecIds.size() - 1);
+                    selectedLabel = labels.get(labels.size() - 1);
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "updateState: Current config: "
+                                        + selectedLabel
+                                        + ", id: "
+                                        + selectedCodecId);
+                    }
+                }
+            }
+
+            setupListPreference(labels, codecIds, selectedLabel, selectedCodecId);
+        }
+    }
+
+    @Override
+    public void onHDAudioEnabled(boolean enabled) {
+        if (DEBUG) {
+            Log.d(TAG, "onHDAudioEnabled: enabled=" + enabled);
+        }
+        if (mListPreference == null) {
+            Log.e(TAG, "onHDAudioEnabled: List preference is null");
+            return;
+        }
+        mListPreference.setEnabled(enabled);
+    }
+
+    @Override
+    protected void writeConfigurationValues(String entryValue) {
+        long codecIdValue = getCodecIdFromEntryValue(entryValue);
+        BluetoothCodecType selectedCodecType = null;
+        BluetoothCodecConfig selectedCodecConfig = null;
+
+        final BluetoothA2dp bluetoothA2dp = mBluetoothA2dp;
+        if (bluetoothA2dp == null) {
+            Log.e(TAG, "writeConfigurationValues: bluetoothA2dp is null");
+            return;
+        }
+
+        final Collection<BluetoothCodecType> codecTypes = bluetoothA2dp.getSupportedCodecTypes();
+        for (BluetoothCodecType codecType : codecTypes) {
+            if (codecType.getCodecId() == codecIdValue) {
+                selectedCodecType = codecType;
+            }
+        }
+
+        if (selectedCodecType == null) {
+            Log.e(
+                    TAG,
+                    "writeConfigurationValues: No selectable codec ID: "
+                            + codecIdValue
+                            + " found. Unable to change codec");
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "writeConfigurationValues: Selected codec: " + selectedCodecType.toString());
+        }
+        final BluetoothCodecStatus codecStatus = getBluetoothCodecStatus();
+        if (codecStatus == null) {
+            Log.e(TAG, "writeConfigurationValues: Bluetooth Codec Status is null");
+            return;
+        }
+
+        final List<BluetoothCodecConfig> codecConfigs =
+                codecStatus.getCodecsSelectableCapabilities();
+        for (BluetoothCodecConfig config : codecConfigs) {
+            BluetoothCodecType codecType = config.getExtendedCodecType();
+            if (codecType == null) {
+                Log.e(TAG, "codec type for config:" + config + " is null");
+            }
+            if (codecType != null && codecType.equals(selectedCodecType)) {
+                selectedCodecConfig = config;
+            }
+        }
+
+        if (selectedCodecConfig == null) {
+            Log.e(
+                    TAG,
+                    "writeConfigurationValues: No selectable codec config for codec: "
+                            + selectedCodecType.toString());
+            return;
+        }
+
+        if (mBluetoothA2dpConfigStore == null) {
+            Log.e(TAG, "writeConfigurationValues: Bluetooth A2dp Config Store is null");
+            return;
+        }
+
+        mBluetoothA2dpConfigStore.setCodecType(selectedCodecType);
+        mBluetoothA2dpConfigStore.setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST);
+        mBluetoothA2dpConfigStore.setSampleRate(
+                AbstractBluetoothDialogPreferenceController.getHighestSampleRate(
+                        selectedCodecConfig));
+        mBluetoothA2dpConfigStore.setBitsPerSample(
+                AbstractBluetoothDialogPreferenceController.getHighestBitsPerSample(
+                        selectedCodecConfig));
+        mBluetoothA2dpConfigStore.setChannelMode(
+                AbstractBluetoothDialogPreferenceController.getHighestChannelMode(
+                        selectedCodecConfig));
+    }
+
+    private long getCodecIdFromEntryValue(String entryValue) {
+        long codecType = BluetoothCodecType.CODEC_ID_SBC;
+        if (entryValue.isEmpty() || Long.valueOf(entryValue) == DEFAULT_VALUE_INT) {
+            return codecType;
+        }
+        return Long.valueOf(entryValue);
+    }
+}
diff --git a/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java b/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java
index 8e583fd..6df100c 100644
--- a/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java
@@ -70,6 +70,7 @@
         for (int simSlotNumber = 1; simSlotNumber < mTelephonyManager.getPhoneCount();
                 simSlotNumber++) {
             final Preference multiSimPreference = createNewPreference(screen.getContext());
+            multiSimPreference.setSelectable(false);
             multiSimPreference.setCopyingEnabled(true);
             multiSimPreference.setOrder(phonePreferenceOrder + simSlotNumber);
             multiSimPreference.setKey(KEY_PHONE_NUMBER + simSlotNumber);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java
index 4017131..e3829e3 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/bugreport/BatteryUsageLogUtils.java
@@ -95,7 +95,7 @@
                         .append(ConvertUtils.utcToLocalTimeForLogging(entry.getTimestamp()))
                         .append(" " + entry.getAction());
         final String description = entry.getActionDescription();
-        if (description != null && !description.isEmpty()) {
+        if (!description.isEmpty()) {
             builder.append(" " + description);
         }
         return builder.toString();
diff --git a/src/com/android/settings/inputmethod/OWNERS b/src/com/android/settings/inputmethod/OWNERS
index 7c7d792..8dc4870 100644
--- a/src/com/android/settings/inputmethod/OWNERS
+++ b/src/com/android/settings/inputmethod/OWNERS
@@ -3,6 +3,7 @@
 include /OWNERS
 
 # Settings for physical keyboard and game pad are better to be reviewed by the input team
-per-file GameControllerPreferenceController.java = file: platform/frameworks/base:/services/core/java/com/android/server/input/OWNERS
-per-file KeyboardLayoutPicker*.java = file: platform/frameworks/base:/services/core/java/com/android/server/input/OWNERS
-per-file PhysicalKeyboard*.java = file: platform/frameworks/base:/services/core/java/com/android/server/input/OWNERS
+per-file GameControllerPreferenceController.java = file:platform/frameworks/base:/INPUT_OWNERS
+per-file KeyboardLayoutPicker*.java = file:platform/frameworks/base:/INPUT_OWNERS
+per-file PhysicalKeyboard*.java = file:platform/frameworks/base:/INPUT_OWNERS
+per-file Trackpad*.java = file:platform/frameworks/base:/INPUT_OWNERS
diff --git a/src/com/android/settings/network/MobileNetworkRepository.java b/src/com/android/settings/network/MobileNetworkRepository.java
index b0c85fc..37f5755 100644
--- a/src/com/android/settings/network/MobileNetworkRepository.java
+++ b/src/com/android/settings/network/MobileNetworkRepository.java
@@ -91,7 +91,6 @@
     private AirplaneModeObserver mAirplaneModeObserver;
     private DataRoamingObserver mDataRoamingObserver;
     private MetricsFeatureProvider mMetricsFeatureProvider;
-    private Map<Integer, MobileDataContentObserver> mDataContentObserverMap = new HashMap<>();
     private int mPhysicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
     private int mLogicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
     private int mCardState = UiccSlotInfo.CARD_STATE_INFO_ABSENT;
@@ -209,6 +208,9 @@
      */
     public void addRegister(LifecycleOwner lifecycleOwner,
             MobileNetworkCallback mobileNetworkCallback, int subId) {
+        if (DEBUG) {
+            Log.d(TAG, "addRegister by SUB ID " + subId);
+        }
         if (sCallbacks.isEmpty()) {
             mSubscriptionManager.addOnSubscriptionsChangedListener(mContext.getMainExecutor(),
                     this);
@@ -222,7 +224,6 @@
         observeAllUiccInfo(lifecycleOwner);
         observeAllMobileNetworkInfo(lifecycleOwner);
         if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            addRegisterBySubId(subId);
             createTelephonyManagerBySubId(subId);
             mDataRoamingObserver.register(mContext, subId);
         }
@@ -231,25 +232,16 @@
         sendAvailableSubInfoCache(mobileNetworkCallback);
     }
 
-    public void addRegisterBySubId(int subId) {
-        MobileDataContentObserver dataContentObserver = new MobileDataContentObserver(
-                new Handler(Looper.getMainLooper()));
-        dataContentObserver.setOnMobileDataChangedListener(() -> {
-            sExecutor.execute(() -> {
-                insertMobileNetworkInfo(mContext, subId,
-                        getTelephonyManagerBySubId(mContext, subId));
-            });
-        });
-        dataContentObserver.register(mContext, subId);
-        mDataContentObserverMap.put(subId, dataContentObserver);
-    }
-
     private void createTelephonyManagerBySubId(int subId) {
-        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                || mTelephonyCallbackMap.containsKey(subId)) {
+            if (DEBUG) {
+                Log.d(TAG, "createTelephonyManagerBySubId: directly return for subId = " + subId);
+            }
             return;
         }
         PhoneCallStateTelephonyCallback
-                telephonyCallback = new PhoneCallStateTelephonyCallback();
+                telephonyCallback = new PhoneCallStateTelephonyCallback(subId);
         TelephonyManager telephonyManager = mContext.getSystemService(
                 TelephonyManager.class).createForSubscriptionId(subId);
         telephonyManager.registerTelephonyCallback(mContext.getMainExecutor(),
@@ -288,10 +280,6 @@
                 }
             }
         }
-        if (mDataContentObserverMap.containsKey(subId)) {
-            mDataContentObserverMap.get(subId).unRegister(mContext);
-            mDataContentObserverMap.remove(subId);
-        }
     }
 
     public void removeRegister(MobileNetworkCallback mobileNetworkCallback) {
@@ -300,10 +288,6 @@
             mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
             mAirplaneModeObserver.unRegister(mContext);
             mDataRoamingObserver.unRegister(mContext);
-            mDataContentObserverMap.forEach((id, observer) -> {
-                observer.unRegister(mContext);
-            });
-            mDataContentObserverMap.clear();
 
             mTelephonyManagerMap.forEach((id, manager) -> {
                 TelephonyCallback callback = mTelephonyCallbackMap.get(id);
@@ -764,7 +748,14 @@
     }
 
     private class PhoneCallStateTelephonyCallback extends TelephonyCallback implements
-            TelephonyCallback.CallStateListener {
+            TelephonyCallback.CallStateListener,
+            TelephonyCallback.UserMobileDataStateListener {
+
+        private int mSubId;
+
+        public PhoneCallStateTelephonyCallback(int subId) {
+            mSubId = subId;
+        }
 
         @Override
         public void onCallStateChanged(int state) {
@@ -772,6 +763,15 @@
                 callback.onCallStateChanged(state);
             }
         }
+
+        @Override
+        public void onUserMobileDataStateChanged(boolean enabled) {
+            Log.d(TAG, "onUserMobileDataStateChanged enabled " + enabled + " on SUB " + mSubId);
+            sExecutor.execute(() -> {
+                insertMobileNetworkInfo(mContext, mSubId,
+                        getTelephonyManagerBySubId(mContext, mSubId));
+            });
+        }
     }
 
     /**
diff --git a/src/com/android/settings/network/telephony/DefaultSubscriptionController.java b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java
index 03ce7f6..fa8760c 100644
--- a/src/com/android/settings/network/telephony/DefaultSubscriptionController.java
+++ b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java
@@ -100,9 +100,6 @@
         mMobileNetworkRepository.addRegister(mLifecycleOwner, this,
                 SubscriptionManager.INVALID_SUBSCRIPTION_ID);
         mMobileNetworkRepository.updateEntity();
-        // Can not get default subId from database until get the callback, add register by subId
-        // later.
-        mMobileNetworkRepository.addRegisterBySubId(getDefaultSubscriptionId());
         mDataSubscriptionChangedReceiver.registerReceiver();
     }
 
@@ -116,8 +113,6 @@
     public void displayPreference(PreferenceScreen screen) {
         super.displayPreference(screen);
         mPreference = screen.findPreference(getPreferenceKey());
-        // Set a summary placeholder to reduce flicker.
-        mPreference.setSummaryProvider(pref -> mContext.getString(R.string.summary_placeholder));
         updateEntries();
     }
 
diff --git a/src/com/android/settings/notification/OWNERS b/src/com/android/settings/notification/OWNERS
index a2ae9ce..29484c6 100644
--- a/src/com/android/settings/notification/OWNERS
+++ b/src/com/android/settings/notification/OWNERS
@@ -1,5 +1,7 @@
 # Default reviewers for this and subdirectories.
+aroederer@google.com
 beverlyt@google.com
 dsandler@android.com
 juliacr@google.com
+matiashe@google.com
 yurilin@google.com
\ No newline at end of file
diff --git a/src/com/android/settings/password/OWNERS b/src/com/android/settings/password/OWNERS
index aa03c59..9cfaf7a 100644
--- a/src/com/android/settings/password/OWNERS
+++ b/src/com/android/settings/password/OWNERS
@@ -1,3 +1,6 @@
+# The Android Biometric team should approve all changes to password subdirectories.
+set noparent
+
 # Default reviewers for this and subdirectories.
 curtislb@google.com
 graciecheng@google.com
diff --git a/src/com/android/settings/security/CredentialStorage.java b/src/com/android/settings/security/CredentialStorage.java
index ea33631..1d8a721 100644
--- a/src/com/android/settings/security/CredentialStorage.java
+++ b/src/com/android/settings/security/CredentialStorage.java
@@ -34,7 +34,7 @@
 import android.security.IKeyChainService;
 import android.security.KeyChain;
 import android.security.KeyChain.KeyChainConnection;
-import android.security.KeyStore;
+import android.security.keystore.KeyProperties;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
@@ -126,9 +126,9 @@
         final Bundle bundle = mInstallBundle;
         mInstallBundle = null;
 
-        final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF);
+        final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyProperties.UID_SELF);
 
-        if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) {
+        if (uid != KeyProperties.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) {
             final int dstUserId = UserHandle.getUserId(uid);
 
             // Restrict install target to the wifi uid.
@@ -279,7 +279,7 @@
 
                 // If this is not a WiFi key, mark  it as user-selectable, so that it can be
                 // selected by users from the Certificate Selection prompt.
-                if (mUid == Process.SYSTEM_UID || mUid == KeyStore.UID_SELF) {
+                if (mUid == Process.SYSTEM_UID || mUid == KeyProperties.UID_SELF) {
                     service.setUserSelectable(mAlias, true);
                 }
 
diff --git a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java
index 675e10f..14ef483 100644
--- a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java
+++ b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java
@@ -17,7 +17,6 @@
 package com.android.settings.wallpaper;
 
 import android.app.WallpaperManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 
@@ -81,10 +80,9 @@
                     SearchIndexableRaw data = new SearchIndexableRaw(context);
                     data.title = controller.getTitle();
                     data.screenTitle = data.title;
-                    ComponentName component = controller.getComponentName();
-                    data.intentTargetPackage = component.getPackageName();
-                    data.intentTargetClass = component.getClassName();
-                    data.intentAction = controller.getComponentActionName();
+                    data.intentTargetPackage = context.getPackageName();
+                    data.intentTargetClass = WallpaperSuggestionActivity.class.getName();
+                    data.intentAction = Intent.ACTION_MAIN;
                     data.key = SUPPORT_SEARCH_INDEX_KEY;
                     data.keywords = controller.getKeywords();
                     result.add(data);
diff --git a/tests/robotests/README.md b/tests/robotests/README.md
index 648f1af..8083f75 100644
--- a/tests/robotests/README.md
+++ b/tests/robotests/README.md
@@ -4,21 +4,20 @@
 ## The full suite
 ```
 $ croot
-$ make RunSettingsRoboTests
+$ atest SettingsRoboTests
 ```
 
 ## Running a single test class
 
+With a filter
+
 ```
 $ croot
-$ make RunSettingsRoboTests ROBOTEST_FILTER=<ClassName>
+$ atest SettingsRoboTests:com.android.settings.display.AdaptiveSleepPreferenceControllerTest
 ```
 
-For example:
+You can also run any single test class with atest (it will try to find the correct path)
 
 ```
-make RunSettingsRoboTests ROBOTEST_FILTER=CodeInspectionTest
+$ atest AdaptiveSleepPreferenceControllerTest
 ```
-
-You can also use partial class name in ROBOTEST_FILTER. If the partial class name matches
-multiple file names, all of them will be executed.
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningAppearancePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningAppearancePreferenceControllerTest.java
index 74fb440..b9de66d 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningAppearancePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningAppearancePreferenceControllerTest.java
@@ -17,8 +17,10 @@
 package com.android.settings.accessibility;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.content.Context;
+import android.os.Looper;
 import android.provider.Settings;
 import android.view.accessibility.CaptioningManager;
 
@@ -68,7 +70,9 @@
 
     @Test
     public void getSummary_smallestScale_shouldReturnExpectedSummary() {
-        mShadowCaptioningManager.setFontScale(0.25f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 0.25f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         final String expectedSummary =
                 getSummaryCombo(/* fontScaleIndex= */ 0, DEFAULT_PRESET_INDEX);
@@ -77,7 +81,9 @@
 
     @Test
     public void getSummary_smallScale_shouldReturnExpectedSummary() {
-        mShadowCaptioningManager.setFontScale(0.5f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 0.5f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         final String expectedSummary =
                 getSummaryCombo(/* fontScaleIndex= */ 1, DEFAULT_PRESET_INDEX);
@@ -86,7 +92,9 @@
 
     @Test
     public void getSummary_mediumScale_shouldReturnExpectedSummary() {
-        mShadowCaptioningManager.setFontScale(1.0f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 1.0f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         final String expectedSummary =
                 getSummaryCombo(/* fontScaleIndex= */ 2, DEFAULT_PRESET_INDEX);
@@ -95,7 +103,9 @@
 
     @Test
     public void getSummary_largeScale_shouldReturnExpectedSummary() {
-        mShadowCaptioningManager.setFontScale(1.5f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 1.5f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         final String expectedSummary =
                 getSummaryCombo(/* fontScaleIndex= */ 3, DEFAULT_PRESET_INDEX);
@@ -104,7 +114,9 @@
 
     @Test
     public void getSummary_largestScale_shouldReturnExpectedSummary() {
-        mShadowCaptioningManager.setFontScale(2.0f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 2.0f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         final String expectedSummary =
                 getSummaryCombo(/* fontScaleIndex= */ 4, DEFAULT_PRESET_INDEX);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundColorControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundColorControllerTest.java
index 0ea7a41..e847f43 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundColorControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundColorControllerTest.java
@@ -127,7 +127,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0xFFFF0000);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundOpacityControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundOpacityControllerTest.java
index 56a61ec..5036ba0 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundOpacityControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningBackgroundOpacityControllerTest.java
@@ -103,7 +103,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0x80FFFFFF);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeColorControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeColorControllerTest.java
index f1a8566..c55e087 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeColorControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeColorControllerTest.java
@@ -101,7 +101,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0xFFFF0000);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeTypeControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeTypeControllerTest.java
index 11871f8..354115f 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeTypeControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningEdgeTypeControllerTest.java
@@ -103,7 +103,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, CaptionStyle.EDGE_TYPE_OUTLINE);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningFontSizeControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningFontSizeControllerTest.java
index 8aeb37e..b00d8d8 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningFontSizeControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningFontSizeControllerTest.java
@@ -23,7 +23,10 @@
 
 import static org.mockito.Mockito.when;
 
+import static org.robolectric.Shadows.shadowOf;
+
 import android.content.Context;
+import android.os.Looper;
 import android.provider.Settings;
 import android.view.accessibility.CaptioningManager;
 
@@ -85,7 +88,9 @@
 
     @Test
     public void updateState_bySmallValue_shouldReturnSmall() {
-        mShadowCaptioningManager.setFontScale(0.5f);
+        Settings.Secure.putFloat(mContext.getContentResolver(),
+            Settings.Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 0.5f);
+        shadowOf(Looper.getMainLooper()).idle();
 
         mController.updateState(mPreference);
 
@@ -94,7 +99,8 @@
 
     @Test
     public void onPreferenceChange_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onPreferenceChange(mPreference, "0.5");
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundColorControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundColorControllerTest.java
index 8991bad..9e9c926 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundColorControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundColorControllerTest.java
@@ -127,7 +127,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0xFFFF0000);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundOpacityControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundOpacityControllerTest.java
index 1ffff60..a88b5f5 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundOpacityControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningForegroundOpacityControllerTest.java
@@ -103,7 +103,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0x80FFFFFF);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningPresetControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningPresetControllerTest.java
index c91baa2..1614c8f 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningPresetControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningPresetControllerTest.java
@@ -103,7 +103,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, CaptionStyle.PRESET_CUSTOM);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningTogglePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningTogglePreferenceControllerTest.java
index e0a04bc..7523db6 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningTogglePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningTogglePreferenceControllerTest.java
@@ -82,7 +82,8 @@
 
     @Test
     public void displayPreference_captionEnabled_shouldSetChecked() {
-        mShadowCaptioningManager.setEnabled(true);
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, ON);
 
         mController.displayPreference(mScreen);
 
@@ -91,7 +92,8 @@
 
     @Test
     public void displayPreference_captionDisabled_shouldSetUnchecked() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
 
         mController.displayPreference(mScreen);
 
@@ -100,7 +102,8 @@
 
     @Test
     public void performClick_captionEnabled_shouldSetCaptionDisabled() {
-        mShadowCaptioningManager.setEnabled(true);
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, ON);
         mController.displayPreference(mScreen);
 
         mSwitchPreference.performClick();
@@ -111,7 +114,8 @@
 
     @Test
     public void performClick_captionDisabled_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mSwitchPreference.performClick();
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningTypefaceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningTypefaceControllerTest.java
index 4d33fb3..aa7d3eb 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningTypefaceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningTypefaceControllerTest.java
@@ -95,7 +95,8 @@
 
     @Test
     public void onPreferenceChange_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onPreferenceChange(mPreference, "serif");
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowColorControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowColorControllerTest.java
index f916778..1258214 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowColorControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowColorControllerTest.java
@@ -128,7 +128,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0xFFFF0000);
diff --git a/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowOpacityControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowOpacityControllerTest.java
index 99eb1e5..0e872a0 100644
--- a/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowOpacityControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/CaptioningWindowOpacityControllerTest.java
@@ -102,7 +102,8 @@
 
     @Test
     public void onValueChanged_shouldSetCaptionEnabled() {
-        mShadowCaptioningManager.setEnabled(false);
+        Settings.Secure.putInt(
+            mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, OFF);
         mController.displayPreference(mScreen);
 
         mController.onValueChanged(mPreference, 0x80FFFFFF);
diff --git a/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
index 0c9906c..e27e3a2 100644
--- a/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/development/Enable16kPagesPreferenceControllerTest.java
@@ -62,13 +62,13 @@
 
     @Test
     public void onSystemPropertyDisabled_shouldDisablePreference() {
-        SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "false");
+        SystemProperties.set(Enable16kUtils.DEV_OPTION_PROPERTY, "false");
         assertThat(mController.isAvailable()).isEqualTo(false);
     }
 
     @Test
     public void onSystemPropertyEnabled_shouldEnablePreference() {
-        SystemProperties.set(Enable16kPagesPreferenceController.DEV_OPTION_PROPERTY, "true");
+        SystemProperties.set(Enable16kUtils.DEV_OPTION_PROPERTY, "true");
         assertThat(mController.isAvailable()).isEqualTo(true);
     }
 
diff --git a/tests/robotests/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceControllerTest.java
new file mode 100644
index 0000000..8abc633
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/bluetooth/AbstractBluetoothListPreferenceControllerTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.development.bluetooth;
+
+import static android.bluetooth.BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.development.BluetoothA2dpConfigStore;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class AbstractBluetoothListPreferenceControllerTest {
+
+    private static final String DEVICE_ADDRESS = "00:11:22:33:44:55";
+
+    private static String DEFAULT_ENTRY;
+    private static final String DEFAULT_ENTRY_VALUE = "1000";
+
+    @Mock private BluetoothA2dp mBluetoothA2dp;
+    @Mock private BluetoothAdapter mBluetoothAdapter;
+    @Mock private PreferenceScreen mScreen;
+
+    private AbstractBluetoothListPreferenceController mController;
+    private ListPreference mPreference;
+    private BluetoothA2dpConfigStore mBluetoothA2dpConfigStore;
+    private BluetoothCodecStatus mCodecStatus;
+    private BluetoothCodecConfig mCodecConfigAAC;
+    private BluetoothCodecConfig mCodecConfigSBC;
+    private BluetoothCodecConfig[] mCodecConfigs = new BluetoothCodecConfig[2];
+    private BluetoothDevice mActiveDevice;
+    private Context mContext;
+    private LifecycleOwner mLifecycleOwner;
+    private Lifecycle mLifecycle;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        mBluetoothA2dpConfigStore = spy(new BluetoothA2dpConfigStore());
+        mActiveDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(DEVICE_ADDRESS);
+        mController =
+                spy(
+                        new AbstractBluetoothListPreferenceControllerImpl(
+                                mContext, mLifecycle, mBluetoothA2dpConfigStore));
+        mController.mBluetoothAdapter = mBluetoothAdapter;
+        mPreference = spy(new ListPreference(mContext));
+
+        mCodecConfigAAC =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC)
+                        .build();
+        mCodecConfigSBC =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC)
+                        .build();
+        mCodecConfigs[0] = mCodecConfigAAC;
+        mCodecConfigs[1] = mCodecConfigSBC;
+
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+        mController.displayPreference(mScreen);
+        when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.A2DP)))
+                .thenReturn(Arrays.asList(mActiveDevice));
+
+        DEFAULT_ENTRY = mContext.getString(R.string.bluetooth_audio_codec_default_selection);
+    }
+
+    private void verifySetupDefaultListPreference() {
+        List<String> entries = new ArrayList<>(1);
+        entries.add(DEFAULT_ENTRY);
+        List<String> entryValues = new ArrayList<>(1);
+        entryValues.add(DEFAULT_ENTRY_VALUE);
+
+        verify(mPreference).setEntries(entries.toArray(new String[entries.size()]));
+        verify(mPreference).setEntryValues(entryValues.toArray(new String[entryValues.size()]));
+        verify(mPreference).setValue(DEFAULT_ENTRY_VALUE);
+        verify(mPreference).setSummary(DEFAULT_ENTRY);
+    }
+
+    @Test
+    public void onPreferenceChange_shouldSetupDefaultListPreference() {
+        mController.onPreferenceChange(mPreference, "" /* new value */);
+        verifySetupDefaultListPreference();
+    }
+
+    @Test
+    public void setupListPreference_wrongSize_shouldSetupDefaultListPreference() {
+        List<String> entries = new ArrayList<>(1);
+        entries.add(DEFAULT_ENTRY);
+        List<String> entryValues = new ArrayList<>(2);
+        entryValues.add(DEFAULT_ENTRY_VALUE);
+        entryValues.add(DEFAULT_ENTRY_VALUE);
+
+        mController.setupListPreference(entries, entryValues, "", "");
+        verifySetupDefaultListPreference();
+    }
+
+    @Test
+    public void setupListPreference_listEmpty_shouldSetupDefaultListPreference() {
+        List<String> entries = new ArrayList<>(1);
+        entries.add(DEFAULT_ENTRY);
+        List<String> entryValues = new ArrayList<>();
+
+        mController.setupListPreference(entries, entryValues, "", "");
+        verifySetupDefaultListPreference();
+    }
+
+    @Test
+    public void getBluetoothCodecStatus_errorChecking() {
+        mController.onBluetoothServiceConnected(null);
+        assertThat(mController.getBluetoothCodecStatus()).isNull();
+
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(null);
+        assertThat(mController.getBluetoothCodecStatus()).isNull();
+    }
+
+    @Test
+    public void getCurrentCodecConfig_errorChecking() {
+        mController.onBluetoothServiceConnected(null);
+        assertThat(mController.getCurrentCodecConfig()).isNull();
+
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(null);
+        assertThat(mController.getCurrentCodecConfig()).isNull();
+    }
+
+    @Test
+    public void getCurrentCodecConfig_verifyConfig() {
+        mCodecStatus = new BluetoothCodecStatus.Builder().setCodecConfig(mCodecConfigAAC).build();
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(mCodecStatus);
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        assertThat(mController.getCurrentCodecConfig()).isEqualTo(mCodecConfigAAC);
+    }
+
+    @Test
+    public void isHDAudioEnabled_errorChecking() {
+        mController.onBluetoothServiceConnected(null);
+        assertFalse(mController.isHDAudioEnabled());
+
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+        when(mBluetoothA2dp.isOptionalCodecsEnabled(mActiveDevice))
+                .thenReturn(BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED);
+        assertFalse(mController.isHDAudioEnabled());
+    }
+
+    @Test
+    public void isHDAudioEnabled_verifyEnabled() {
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+        when(mBluetoothA2dp.isOptionalCodecsEnabled(mActiveDevice))
+                .thenReturn(BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+        assertTrue(mController.isHDAudioEnabled());
+    }
+
+    @Test
+    public void onBluetoothServiceConnected_verifyBluetoothA2dpConfigStore() {
+        mCodecStatus =
+                new BluetoothCodecStatus.Builder()
+                        .setCodecConfig(mCodecConfigAAC)
+                        .setCodecsSelectableCapabilities(Arrays.asList(mCodecConfigs))
+                        .build();
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(mCodecStatus);
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        verify(mBluetoothA2dpConfigStore).setCodecType(mCodecConfigAAC.getExtendedCodecType());
+        verify(mBluetoothA2dpConfigStore).setSampleRate(mCodecConfigAAC.getSampleRate());
+        verify(mBluetoothA2dpConfigStore).setBitsPerSample(mCodecConfigAAC.getBitsPerSample());
+        verify(mBluetoothA2dpConfigStore).setChannelMode(mCodecConfigAAC.getChannelMode());
+        verify(mBluetoothA2dpConfigStore).setCodecPriority(CODEC_PRIORITY_HIGHEST);
+        verify(mBluetoothA2dpConfigStore)
+                .setCodecSpecific1Value(mCodecConfigAAC.getCodecSpecific1());
+    }
+
+    private static class AbstractBluetoothListPreferenceControllerImpl
+            extends AbstractBluetoothListPreferenceController {
+
+        private AbstractBluetoothListPreferenceControllerImpl(
+                Context context, Lifecycle lifecycle, BluetoothA2dpConfigStore store) {
+            super(context, lifecycle, store);
+        }
+
+        @Override
+        public String getPreferenceKey() {
+            return "KEY";
+        }
+
+        @Override
+        protected void writeConfigurationValues(String entryValue) {}
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceControllerTest.java
new file mode 100644
index 0000000..fab867f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/bluetooth/BluetoothCodecListPreferenceControllerTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.development.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothCodecType;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.development.BluetoothA2dpConfigStore;
+import com.android.settings.development.Flags;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothCodecListPreferenceControllerTest {
+    private static final String DEVICE_ADDRESS = "00:11:22:33:44:55";
+
+    @Mock private BluetoothA2dp mBluetoothA2dp;
+    @Mock private BluetoothAdapter mBluetoothAdapter;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private AbstractBluetoothPreferenceController.Callback mCallback;
+
+    private BluetoothCodecListPreferenceController mController;
+    private ListPreference mPreference;
+    private BluetoothA2dpConfigStore mBluetoothA2dpConfigStore;
+    private BluetoothCodecStatus mCodecStatus;
+    private BluetoothCodecType mCodecTypeAAC;
+    private BluetoothCodecType mCodecTypeSBC;
+    private BluetoothCodecType mCodecTypeAPTX;
+    private BluetoothCodecType mCodecTypeLDAC;
+    private BluetoothCodecType mCodecTypeOPUS;
+    private List<BluetoothCodecType> mCodecTypes;
+
+    private BluetoothCodecConfig mCodecConfigAAC;
+    private BluetoothCodecConfig mCodecConfigSBC;
+    private BluetoothCodecConfig mCodecConfigAPTX;
+    private BluetoothCodecConfig mCodecConfigAPTXHD;
+    private BluetoothCodecConfig mCodecConfigLDAC;
+    private BluetoothCodecConfig mCodecConfigOPUS;
+    private List<BluetoothCodecConfig> mCodecConfigs;
+    private BluetoothDevice mActiveDevice;
+    private Context mContext;
+    private LifecycleOwner mLifecycleOwner;
+    private Lifecycle mLifecycle;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        mBluetoothA2dpConfigStore = spy(new BluetoothA2dpConfigStore());
+        mActiveDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(DEVICE_ADDRESS);
+        mController =
+                new BluetoothCodecListPreferenceController(
+                        mContext, mLifecycle, mBluetoothA2dpConfigStore, mCallback);
+        mController.mBluetoothAdapter = mBluetoothAdapter;
+        mPreference = new ListPreference(mContext);
+        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
+        mController.displayPreference(mScreen);
+
+        mCodecTypeAAC =
+                BluetoothCodecType.createFromType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC);
+        mCodecTypeSBC =
+                BluetoothCodecType.createFromType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC);
+        mCodecTypeAPTX =
+                BluetoothCodecType.createFromType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX);
+        mCodecTypeLDAC =
+                BluetoothCodecType.createFromType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC);
+        mCodecTypeOPUS =
+                BluetoothCodecType.createFromType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_OPUS);
+
+        mCodecTypes = new ArrayList<>();
+        mCodecTypes.addAll(
+                Arrays.asList(
+                        mCodecTypeSBC,
+                        mCodecTypeAAC,
+                        mCodecTypeAPTX,
+                        mCodecTypeLDAC,
+                        mCodecTypeOPUS));
+
+        mCodecConfigSBC =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC)
+                        .setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)
+                        .setSampleRate(
+                                BluetoothCodecConfig.SAMPLE_RATE_96000
+                                        | BluetoothCodecConfig.SAMPLE_RATE_176400)
+                        .setBitsPerSample(BluetoothCodecConfig.BITS_PER_SAMPLE_32)
+                        .setChannelMode(
+                                BluetoothCodecConfig.CHANNEL_MODE_MONO
+                                        | BluetoothCodecConfig.CHANNEL_MODE_STEREO)
+                        .build();
+        mCodecConfigAAC =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC)
+                        .setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST)
+                        .setSampleRate(
+                                BluetoothCodecConfig.SAMPLE_RATE_48000
+                                        | BluetoothCodecConfig.SAMPLE_RATE_88200)
+                        .setBitsPerSample(
+                                BluetoothCodecConfig.BITS_PER_SAMPLE_16
+                                        | BluetoothCodecConfig.BITS_PER_SAMPLE_24)
+                        .setChannelMode(BluetoothCodecConfig.CHANNEL_MODE_STEREO)
+                        .build();
+        mCodecConfigAPTX =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX)
+                        .build();
+        mCodecConfigAPTXHD =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD)
+                        .build();
+        mCodecConfigLDAC =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC)
+                        .build();
+        mCodecConfigOPUS =
+                new BluetoothCodecConfig.Builder()
+                        .setCodecType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_OPUS)
+                        .build();
+
+        mCodecConfigs = new ArrayList<>();
+        mCodecConfigs.addAll(
+                Arrays.asList(
+                        mCodecConfigOPUS,
+                        mCodecConfigAAC,
+                        mCodecConfigSBC,
+                        mCodecConfigAPTX,
+                        mCodecConfigAPTXHD,
+                        mCodecConfigLDAC));
+
+        when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.A2DP)))
+                .thenReturn(Arrays.asList(mActiveDevice));
+        when(mBluetoothA2dp.getSupportedCodecTypes()).thenReturn(mCodecTypes);
+    }
+
+    @Test
+    public void writeConfigurationValues_selectDefault() {
+        mCodecStatus =
+                new BluetoothCodecStatus.Builder()
+                        .setCodecConfig(mCodecConfigSBC)
+                        .setCodecsSelectableCapabilities(mCodecConfigs)
+                        .build();
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(mCodecStatus);
+        when(mBluetoothA2dp.isOptionalCodecsEnabled(mActiveDevice))
+                .thenReturn(BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        mController.writeConfigurationValues(String.valueOf(mController.DEFAULT_VALUE_INT));
+        verify(mBluetoothA2dpConfigStore, times(2)).setCodecType(mCodecTypeSBC);
+    }
+
+    @Test
+    public void writeConfigurationValues_checkCodec() {
+        mCodecStatus =
+                new BluetoothCodecStatus.Builder()
+                        .setCodecConfig(mCodecConfigSBC)
+                        .setCodecsSelectableCapabilities(mCodecConfigs)
+                        .build();
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(mCodecStatus);
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeSBC.getCodecId()));
+        verify(mBluetoothA2dpConfigStore, atLeastOnce()).setCodecType(mCodecTypeSBC);
+
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeAAC.getCodecId()));
+        verify(mBluetoothA2dpConfigStore).setCodecType(mCodecTypeAAC);
+
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeAPTX.getCodecId()));
+        verify(mBluetoothA2dpConfigStore).setCodecType(mCodecTypeAPTX);
+
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeLDAC.getCodecId()));
+        verify(mBluetoothA2dpConfigStore).setCodecType(mCodecTypeLDAC);
+
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeOPUS.getCodecId()));
+        verify(mBluetoothA2dpConfigStore).setCodecType(mCodecTypeOPUS);
+    }
+
+    @Test
+    public void writeConfigurationValues_chooseHighestConfig() {
+        mCodecStatus =
+                new BluetoothCodecStatus.Builder()
+                        .setCodecConfig(mCodecConfigSBC)
+                        .setCodecsSelectableCapabilities((mCodecConfigs))
+                        .build();
+        when(mBluetoothA2dp.getCodecStatus(mActiveDevice)).thenReturn(mCodecStatus);
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+        mController.writeConfigurationValues(String.valueOf(mCodecTypeAAC.getCodecId()));
+
+        verify(mBluetoothA2dpConfigStore, atLeastOnce())
+                .setCodecPriority(BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST);
+        verify(mBluetoothA2dpConfigStore, atLeastOnce())
+                .setSampleRate(BluetoothCodecConfig.SAMPLE_RATE_88200);
+        verify(mBluetoothA2dpConfigStore, atLeastOnce())
+                .setBitsPerSample(BluetoothCodecConfig.BITS_PER_SAMPLE_24);
+        verify(mBluetoothA2dpConfigStore, atLeastOnce())
+                .setChannelMode(BluetoothCodecConfig.CHANNEL_MODE_STEREO);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_A2DP_OFFLOAD_CODEC_EXTENSIBILITY_SETTINGS)
+    public void onPreferenceChange_notifyPreference() {
+        assertFalse(
+                mController.onPreferenceChange(
+                        mPreference, String.valueOf(mCodecTypeAAC.getCodecId())));
+
+        mController.onBluetoothServiceConnected(mBluetoothA2dp);
+
+        assertTrue(
+                mController.onPreferenceChange(
+                        mPreference, String.valueOf(mCodecTypeAAC.getCodecId())));
+
+        verify(mCallback).onBluetoothCodecChanged();
+    }
+
+    @Test
+    public void onHDAudioEnabled_setsPreferenceEnabled() {
+        mController.onHDAudioEnabled(/* enabled= */ true);
+        assertThat(mPreference.isEnabled()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/sound/MediaOutputPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/sound/MediaOutputPreferenceControllerTest.java
index b9f9b16..9db8324 100644
--- a/tests/robotests/src/com/android/settings/sound/MediaOutputPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/sound/MediaOutputPreferenceControllerTest.java
@@ -234,8 +234,6 @@
         mScreen.addPreference(mPreference);
         mController.displayPreference(mScreen);
         mController.setCallback(mAudioSwitchPreferenceCallback);
-
-        mSetFlagsRule.initAllFlagsToReleaseConfigDefault();
     }
 
     @After
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowKeyStore.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowKeyStore.java
deleted file mode 100644
index 99eca0a..0000000
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowKeyStore.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2017 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.testutils.shadow;
-
-import android.security.KeyStore;
-
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.Resetter;
-
-@Implements(KeyStore.class)
-public class ShadowKeyStore {
-
-    private static boolean sIsHardwareBacked;
-
-    @Resetter
-    public static void reset() {
-        sIsHardwareBacked = false;
-    }
-
-    @Implementation
-    protected boolean isHardwareBacked() {
-        return sIsHardwareBacked;
-    }
-
-    public static void setHardwareBacked(boolean hardwareBacked) {
-        sIsHardwareBacked = hardwareBacked;
-    }
-}
diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml
index 51ac1b7..1950f20 100644
--- a/tests/spa_unit/AndroidManifest.xml
+++ b/tests/spa_unit/AndroidManifest.xml
@@ -19,11 +19,12 @@
           xmlns:tools="http://schemas.android.com/tools"
           package="com.android.settings.tests.spa_unit">
 
+    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
     <uses-permission android:name="android.permission.MANAGE_APPOPS" />
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
-    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
-    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+    <uses-permission android:name="com.android.settings.BATTERY_DATA" />
 
     <application android:debuggable="true">
         <provider android:name="com.android.settings.slices.SettingsSliceProvider"
diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt
deleted file mode 100644
index 644095d..0000000
--- a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * 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.threadnetwork
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.net.thread.ThreadNetworkController.STATE_DISABLED
-import android.net.thread.ThreadNetworkController.STATE_DISABLING
-import android.net.thread.ThreadNetworkController.STATE_ENABLED
-import android.net.thread.ThreadNetworkController.StateCallback
-import android.net.thread.ThreadNetworkException
-import android.os.OutcomeReceiver
-import android.platform.test.flag.junit.SetFlagsRule
-import android.provider.Settings
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.preference.PreferenceManager
-import androidx.preference.SwitchPreference
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.net.thread.platform.flags.Flags
-import com.android.settings.R
-import com.android.settings.core.BasePreferenceController.AVAILABLE
-import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE
-import com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING
-import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
-import com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController.BaseThreadNetworkController
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.verify
-import java.util.concurrent.Executor
-
-/** Unit tests for [ThreadNetworkPreferenceController].  */
-@RunWith(AndroidJUnit4::class)
-class ThreadNetworkPreferenceControllerTest {
-    @get:Rule
-    val mSetFlagsRule = SetFlagsRule()
-    private lateinit var context: Context
-    private lateinit var executor: Executor
-    private lateinit var controller: ThreadNetworkPreferenceController
-    private lateinit var fakeThreadNetworkController: FakeThreadNetworkController
-    private lateinit var preference: SwitchPreference
-    private val broadcastReceiverArgumentCaptor = ArgumentCaptor.forClass(
-        BroadcastReceiver::class.java
-    )
-
-    @Before
-    fun setUp() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_ENABLED_PLATFORM)
-        context = spy(ApplicationProvider.getApplicationContext<Context>())
-        executor = ContextCompat.getMainExecutor(context)
-        fakeThreadNetworkController = FakeThreadNetworkController(executor)
-        controller = newControllerWithThreadFeatureSupported(true)
-        val preferenceManager = PreferenceManager(context)
-        val preferenceScreen = preferenceManager.createPreferenceScreen(context)
-        preference = SwitchPreference(context)
-        preference.key = "thread_network_settings"
-        preferenceScreen.addPreference(preference)
-        controller.displayPreference(preferenceScreen)
-
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0)
-    }
-
-    private fun newControllerWithThreadFeatureSupported(
-        present: Boolean
-    ): ThreadNetworkPreferenceController {
-        return ThreadNetworkPreferenceController(
-            context,
-            "thread_network_settings" /* key */,
-            executor,
-            if (present) fakeThreadNetworkController else null
-        )
-    }
-
-    @Test
-    fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_ENABLED_PLATFORM)
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE)
-    }
-
-    @Test
-    fun availabilityStatus_airPlaneModeOn_returnsDisabledDependentSetting() {
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1)
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING)
-    }
-
-    @Test
-    fun availabilityStatus_airPlaneModeOff_returnsAvailable() {
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0)
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE)
-    }
-
-    @Test
-    fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() {
-        controller = newControllerWithThreadFeatureSupported(false)
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(fakeThreadNetworkController.registeredStateCallback).isNull()
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE)
-    }
-
-    @Test
-    fun isChecked_threadSetEnabled_returnsTrue() {
-        fakeThreadNetworkController.setEnabled(true, executor) { }
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(controller.isChecked).isTrue()
-    }
-
-    @Test
-    fun isChecked_threadSetDisabled_returnsFalse() {
-        fakeThreadNetworkController.setEnabled(false, executor) { }
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(controller.isChecked).isFalse()
-    }
-
-    @Test
-    fun setChecked_setChecked_threadIsEnabled() {
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        controller.setChecked(true)
-
-        assertThat(fakeThreadNetworkController.isEnabled).isTrue()
-    }
-
-    @Test
-    fun setChecked_setUnchecked_threadIsDisabled() {
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        controller.setChecked(false)
-
-        assertThat(fakeThreadNetworkController.isEnabled).isFalse()
-    }
-
-    @Test
-    fun updatePreference_airPlaneModeOff_preferenceEnabled() {
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0)
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(preference.isEnabled).isTrue()
-        assertThat(preference.summary).isEqualTo(
-            context.resources.getString(R.string.thread_network_settings_summary)
-        )
-    }
-
-    @Test
-    fun updatePreference_airPlaneModeOn_preferenceDisabled() {
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1)
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-
-        assertThat(preference.isEnabled).isFalse()
-        assertThat(preference.summary).isEqualTo(
-            context.resources.getString(R.string.thread_network_settings_summary_airplane_mode)
-        )
-    }
-
-    @Test
-    fun updatePreference_airPlaneModeTurnedOn_preferenceDisabled() {
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0)
-        startControllerAndCaptureCallbacks()
-
-        Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1)
-        broadcastReceiverArgumentCaptor.value.onReceive(context, Intent())
-
-        assertThat(preference.isEnabled).isFalse()
-        assertThat(preference.summary).isEqualTo(
-            context.resources.getString(R.string.thread_network_settings_summary_airplane_mode)
-        )
-    }
-
-    private fun startControllerAndCaptureCallbacks() {
-        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
-        verify(context)!!.registerReceiver(broadcastReceiverArgumentCaptor.capture(), any())
-    }
-
-    private class FakeThreadNetworkController(private val executor: Executor) :
-        BaseThreadNetworkController {
-        var isEnabled = true
-            private set
-        var registeredStateCallback: StateCallback? = null
-            private set
-
-        override fun setEnabled(
-            enabled: Boolean,
-            executor: Executor,
-            receiver: OutcomeReceiver<Void?, ThreadNetworkException>
-        ) {
-            isEnabled = enabled
-            if (registeredStateCallback != null) {
-                if (!isEnabled) {
-                    executor.execute {
-                        registeredStateCallback!!.onThreadEnableStateChanged(
-                            STATE_DISABLING
-                        )
-                    }
-                    executor.execute {
-                        registeredStateCallback!!.onThreadEnableStateChanged(
-                            STATE_DISABLED
-                        )
-                    }
-                } else {
-                    executor.execute {
-                        registeredStateCallback!!.onThreadEnableStateChanged(
-                            STATE_ENABLED
-                        )
-                    }
-                }
-            }
-            executor.execute { receiver.onResult(null) }
-        }
-
-        override fun registerStateCallback(
-            executor: Executor,
-            callback: StateCallback
-        ) {
-            require(callback !== registeredStateCallback) { "callback is already registered" }
-            registeredStateCallback = callback
-            val enabledState =
-                if (isEnabled) STATE_ENABLED else STATE_DISABLED
-            executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) }
-        }
-
-        override fun unregisterStateCallback(callback: StateCallback) {
-            requireNotNull(registeredStateCallback) { "callback is already unregistered" }
-            registeredStateCallback = null
-        }
-    }
-}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt
new file mode 100644
index 0000000..e30226e
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.threadnetwork
+
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+import java.util.concurrent.Executor
+
+/** A fake implementation of [BaseThreadNetworkController] for unit tests. */
+class FakeThreadNetworkController : BaseThreadNetworkController {
+    var isEnabled = false
+        private set
+    var registeredStateCallback: ThreadNetworkController.StateCallback? = null
+        private set
+
+    override fun setEnabled(
+        enabled: Boolean,
+        executor: Executor,
+        receiver: OutcomeReceiver<Void?, ThreadNetworkException>
+    ) {
+        isEnabled = enabled
+        if (registeredStateCallback != null) {
+            if (!isEnabled) {
+                executor.execute {
+                    registeredStateCallback!!.onThreadEnableStateChanged(
+                        ThreadNetworkController.STATE_DISABLING
+                    )
+                }
+                executor.execute {
+                    registeredStateCallback!!.onThreadEnableStateChanged(
+                        ThreadNetworkController.STATE_DISABLED
+                    )
+                }
+            } else {
+                executor.execute {
+                    registeredStateCallback!!.onThreadEnableStateChanged(
+                        ThreadNetworkController.STATE_ENABLED
+                    )
+                }
+            }
+        }
+        executor.execute { receiver.onResult(null) }
+    }
+
+    override fun registerStateCallback(
+        executor: Executor,
+        callback: ThreadNetworkController.StateCallback
+    ) {
+        require(callback !== registeredStateCallback) { "callback is already registered" }
+        registeredStateCallback = callback
+        val enabledState =
+            if (isEnabled) ThreadNetworkController.STATE_ENABLED else ThreadNetworkController.STATE_DISABLED
+        executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) }
+    }
+
+    override fun unregisterStateCallback(callback: ThreadNetworkController.StateCallback) {
+        requireNotNull(registeredStateCallback) { "callback is already unregistered" }
+        registeredStateCallback = null
+    }
+}
diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS
similarity index 100%
rename from tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS
rename to tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt
new file mode 100644
index 0000000..13e4291
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.core.BasePreferenceController.AVAILABLE
+import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE
+import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
+import com.android.settings.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import java.util.concurrent.Executor
+
+/** Unit tests for [ThreadNetworkFragmentController].  */
+@RunWith(AndroidJUnit4::class)
+class ThreadNetworkFragmentControllerTest {
+    @get:Rule
+    val mSetFlagsRule = SetFlagsRule()
+    private lateinit var context: Context
+    private lateinit var executor: Executor
+    private lateinit var controller: ThreadNetworkFragmentController
+    private lateinit var fakeThreadNetworkController: FakeThreadNetworkController
+
+    @Before
+    fun setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+        context = spy(ApplicationProvider.getApplicationContext<Context>())
+        executor = MoreExecutors.directExecutor()
+        fakeThreadNetworkController = FakeThreadNetworkController()
+        controller = newControllerWithThreadFeatureSupported(true)
+    }
+
+    private fun newControllerWithThreadFeatureSupported(
+        present: Boolean
+    ): ThreadNetworkFragmentController {
+        return ThreadNetworkFragmentController(
+            context,
+            "thread_network_settings" /* key */,
+            executor,
+            if (present) fakeThreadNetworkController else null
+        )
+    }
+
+    @Test
+    fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+        startController(controller)
+
+        assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
+    }
+
+    @Test
+    fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() {
+        controller = newControllerWithThreadFeatureSupported(false)
+        startController(controller)
+
+        assertThat(fakeThreadNetworkController.registeredStateCallback).isNull()
+        assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
+    }
+
+    @Test
+    fun availabilityStatus_threadFeatureSupported_returnsAvailable() {
+        controller = newControllerWithThreadFeatureSupported(true)
+        startController(controller)
+
+        assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE)
+    }
+
+    @Test
+    fun getSummary_ThreadIsEnabled_returnsOn() {
+        startController(controller)
+        fakeThreadNetworkController.setEnabled(true, executor) {}
+
+        assertThat(controller.summary).isEqualTo("On")
+    }
+
+    @Test
+    fun getSummary_ThreadIsDisabled_returnsOff() {
+        startController(controller)
+        fakeThreadNetworkController.setEnabled(false, executor) {}
+
+        assertThat(controller.summary).isEqualTo("Off")
+    }
+
+    private fun startController(controller: ThreadNetworkFragmentController) {
+        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
+    }
+}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt
new file mode 100644
index 0000000..065ff96
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.threadnetwork
+
+import android.content.Context
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.preference.PreferenceManager
+import androidx.preference.SwitchPreference
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE
+import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
+import com.android.settings.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import java.util.concurrent.Executor
+
+/** Unit tests for [ThreadNetworkToggleController].  */
+@RunWith(AndroidJUnit4::class)
+class ThreadNetworkToggleControllerTest {
+    @get:Rule
+    val mSetFlagsRule = SetFlagsRule()
+    private lateinit var context: Context
+    private lateinit var executor: Executor
+    private lateinit var controller: ThreadNetworkToggleController
+    private lateinit var fakeThreadNetworkController: FakeThreadNetworkController
+    private lateinit var preference: SwitchPreference
+
+    @Before
+    fun setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+        context = spy(ApplicationProvider.getApplicationContext<Context>())
+        executor = MoreExecutors.directExecutor()
+        fakeThreadNetworkController = FakeThreadNetworkController()
+        controller = newControllerWithThreadFeatureSupported(true)
+        val preferenceManager = PreferenceManager(context)
+        val preferenceScreen = preferenceManager.createPreferenceScreen(context)
+        preference = SwitchPreference(context)
+        preference.key = "toggle_thread_network"
+        preferenceScreen.addPreference(preference)
+        controller.displayPreference(preferenceScreen)
+    }
+
+    private fun newControllerWithThreadFeatureSupported(
+        present: Boolean
+    ): ThreadNetworkToggleController {
+        return ThreadNetworkToggleController(
+            context,
+            "toggle_thread_network" /* key */,
+            executor,
+            if (present) fakeThreadNetworkController else null
+        )
+    }
+
+    @Test
+    fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+        assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
+    }
+
+    @Test
+    fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() {
+        controller = newControllerWithThreadFeatureSupported(false)
+        startController(controller)
+
+        assertThat(fakeThreadNetworkController.registeredStateCallback).isNull()
+        assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
+    }
+
+    @Test
+    fun isChecked_threadSetEnabled_returnsTrue() {
+        fakeThreadNetworkController.setEnabled(true, executor) { }
+        startController(controller)
+
+        assertThat(controller.isChecked).isTrue()
+    }
+
+    @Test
+    fun isChecked_threadSetDisabled_returnsFalse() {
+        fakeThreadNetworkController.setEnabled(false, executor) { }
+        startController(controller)
+
+        assertThat(controller.isChecked).isFalse()
+    }
+
+    @Test
+    fun setChecked_setChecked_threadIsEnabled() {
+        startController(controller)
+
+        controller.setChecked(true)
+
+        assertThat(fakeThreadNetworkController.isEnabled).isTrue()
+    }
+
+    @Test
+    fun setChecked_setUnchecked_threadIsDisabled() {
+        startController(controller)
+
+        controller.setChecked(false)
+
+        assertThat(fakeThreadNetworkController.isEnabled).isFalse()
+    }
+
+    private fun startController(controller: ThreadNetworkToggleController) {
+        controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
+    }
+}