Merge "Add flag for the Legal information page migration" into main
diff --git a/res/drawable/ic_head_tracking.xml b/res/drawable/ic_head_tracking.xml
new file mode 100644
index 0000000..d4a44fd
--- /dev/null
+++ b/res/drawable/ic_head_tracking.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M480,520Q414,520 367,473Q320,426 320,360Q320,294 367,247Q414,200 480,200Q546,200 593,247Q640,294 640,360Q640,426 593,473Q546,520 480,520ZM160,840L160,728Q160,695 177,666Q194,637 224,622Q275,596 339,578Q403,560 480,560Q557,560 621,578Q685,596 736,622Q766,637 783,666Q800,695 800,728L800,840L160,840ZM240,760L720,760L720,728Q720,717 714.5,708Q709,699 700,694Q664,676 607.5,658Q551,640 480,640Q409,640 352.5,658Q296,676 260,694Q251,699 245.5,708Q240,717 240,728L240,760ZM480,440Q513,440 536.5,416.5Q560,393 560,360Q560,327 536.5,303.5Q513,280 480,280Q447,280 423.5,303.5Q400,327 400,360Q400,393 423.5,416.5Q447,440 480,440ZM39,200L39,120Q56,120 70,113.5Q84,107 95,96Q106,85 112,71Q118,57 118,40L199,40Q199,73 186.5,102Q174,131 152,153Q130,175 101,187.5Q72,200 39,200ZM39,361L39,281Q90,281 133.5,262Q177,243 209,210Q241,177 260,133.5Q279,90 279,40L360,40Q360,106 335,164.5Q310,223 266,267Q222,311 164,336Q106,361 39,361ZM920,361Q854,361 795.5,336Q737,311 693,267Q649,223 624,164.5Q599,106 599,40L679,40Q679,90 698,133.5Q717,177 750,210Q783,243 826.5,262Q870,281 920,281L920,361ZM920,200Q887,200 858,187.5Q829,175 807,153Q785,131 772.5,102Q760,73 760,40L840,40Q840,57 846.5,71Q853,85 864,96Q875,107 889,113.5Q903,120 920,120L920,200ZM480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360ZM480,760L480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760Q480,760 480,760L480,760L480,760Z" />
+</vector>
diff --git a/res/drawable/ic_spatial_audio.xml b/res/drawable/ic_spatial_audio.xml
new file mode 100644
index 0000000..0ee609a
--- /dev/null
+++ b/res/drawable/ic_spatial_audio.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M920,401Q848,401 782,373.5Q716,346 665,295Q614,244 586.5,178Q559,112 559,40L639,40Q639,97 660,148Q681,199 721,239Q761,279 812,300.5Q863,322 920,322L920,401ZM920,242Q879,242 842.5,227Q806,212 777,183Q748,154 733,117.5Q718,81 718,40L797,40Q797,65 806.5,87.5Q816,110 833,127Q850,144 872.5,153Q895,162 920,162L920,242ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
+</vector>
diff --git a/res/drawable/ic_spatial_audio_off.xml b/res/drawable/ic_spatial_audio_off.xml
new file mode 100644
index 0000000..c7d3272
--- /dev/null
+++ b/res/drawable/ic_spatial_audio_off.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M750,550L806,494Q766,454 743.5,402.5Q721,351 721,294Q721,237 743.5,186Q766,135 806,95L750,37Q699,88 670,155Q641,222 641,294Q641,366 670,432.5Q699,499 750,550ZM862,436L918,380Q901,363 891,341Q881,319 881,294Q881,269 891,247Q901,225 918,208L862,151Q833,180 817,216Q801,252 801,293Q801,334 817,371Q833,408 862,436ZM400,520Q334,520 287,473Q240,426 240,360Q240,294 287,247Q334,200 400,200Q466,200 513,247Q560,294 560,360Q560,426 513,473Q466,520 400,520ZM80,840L80,728Q80,695 97,666Q114,637 144,622Q195,596 259,578Q323,560 400,560Q477,560 541,578Q605,596 656,622Q686,637 703,666Q720,695 720,728L720,840L80,840ZM160,760L640,760L640,728Q640,717 634.5,708Q629,699 620,694Q584,676 527.5,658Q471,640 400,640Q329,640 272.5,658Q216,676 180,694Q171,699 165.5,708Q160,717 160,728L160,760ZM400,440Q433,440 456.5,416.5Q480,393 480,360Q480,327 456.5,303.5Q433,280 400,280Q367,280 343.5,303.5Q320,327 320,360Q320,393 343.5,416.5Q367,440 400,440ZM400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360Q400,360 400,360ZM400,760L400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760Q400,760 400,760L400,760L400,760Z" />
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6c018c2..4b30dc1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7946,6 +7946,18 @@
     <!-- Sound: Footer hyperlink text to launch the Connected devices settings page. [CHAR LIMIT=NONE]-->
     <string name="spatial_audio_footer_learn_more_text">Connected devices settings</string>
 
+    <!-- Bluetooth device details: spatial audio multi-toggle title. [CHAR LIMIT=20]-->
+    <string name="spatial_audio_multi_toggle_title">Spatial Audio</string>
+
+    <!-- Bluetooth device details: spatial audio is off. [CHAR LIMIT=20]-->
+    <string name="spatial_audio_multi_toggle_off">Off</string>
+
+    <!-- Bluetooth device details: spatial audio is on. [CHAR LIMIT=20]-->
+    <string name="spatial_audio_multi_toggle_on">Off</string>
+
+    <!-- Bluetooth device details: head tracking is on. [CHAR LIMIT=20]-->
+    <string name="spatial_audio_multi_toggle_head_tracking_on">Off</string>
+
     <!-- Zen Modes: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
     <string name="zen_mode_settings_schedules_summary">
         {count, plural,
diff --git a/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java b/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java
new file mode 100644
index 0000000..459dbb9
--- /dev/null
+++ b/src/com/android/settings/accessibility/DaltonizerPreferenceUtil.java
@@ -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.accessibility;
+
+import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
+import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.view.accessibility.AccessibilityManager;
+
+import com.google.common.primitives.Ints;
+
+/**
+ * Utility class for retrieving accessibility daltonizer related values in secure settings.
+ */
+public class DaltonizerPreferenceUtil {
+
+    /**
+     * Return the daltonizer display mode stored in
+     * {@link Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER}.
+     * By default it returns {@link DALTONIZER_CORRECT_DEUTERANOMALY}.
+     */
+    public static int getSecureAccessibilityDaltonizerValue(ContentResolver resolver) {
+        final String daltonizerStringValue = Settings.Secure.getString(
+                resolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER);
+        if (daltonizerStringValue == null) {
+            return AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY;
+        }
+        final Integer daltonizerIntValue = Ints.tryParse(daltonizerStringValue);
+        return daltonizerIntValue == null ? AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY
+                : daltonizerIntValue;
+    }
+
+    /**
+     * Returns the daltonizer enabled value in
+     * {@link Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED}.
+     * By default it returns false.
+     */
+    public static boolean isSecureAccessibilityDaltonizerEnabled(ContentResolver resolver) {
+        return Settings.Secure.getInt(
+                resolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+                OFF) == ON;
+    }
+}
diff --git a/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java b/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java
index 296536c..5a8c710 100644
--- a/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java
+++ b/src/com/android/settings/accessibility/DaltonizerRadioButtonPreferenceController.java
@@ -24,7 +24,6 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.view.View;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.DefaultLifecycleObserver;
@@ -36,8 +35,6 @@
 import com.android.settings.core.BasePreferenceController;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
-import com.google.common.primitives.Ints;
-
 import java.util.HashMap;
 import java.util.Map;
 
@@ -70,17 +67,6 @@
         };
     }
 
-    protected static int getSecureAccessibilityDaltonizerValue(ContentResolver resolver) {
-        final String daltonizerStringValue = Settings.Secure.getString(
-                resolver, DALTONIZER_TYPE_SETTINGS_KEY);
-        if (daltonizerStringValue == null) {
-            return AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY;
-        }
-        final Integer daltonizerIntValue = Ints.tryParse(daltonizerStringValue);
-        return daltonizerIntValue == null ? AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY
-                : daltonizerIntValue;
-    }
-
     private Map<String, Integer> getDaltonizerValueToKeyMap() {
         if (mAccessibilityDaltonizerKeyToValueMap.isEmpty()) {
 
@@ -123,7 +109,8 @@
     }
 
     private int getAccessibilityDaltonizerValue() {
-        final int daltonizerValue = getSecureAccessibilityDaltonizerValue(mContentResolver);
+        final int daltonizerValue =
+                DaltonizerPreferenceUtil.getSecureAccessibilityDaltonizerValue(mContentResolver);
         return daltonizerValue;
     }
 
diff --git a/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java b/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java
index 2997185..6a9977a 100644
--- a/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java
+++ b/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceController.java
@@ -15,6 +15,9 @@
  */
 package com.android.settings.accessibility;
 
+import static com.android.settings.accessibility.DaltonizerPreferenceUtil.isSecureAccessibilityDaltonizerEnabled;
+import static com.android.settings.accessibility.DaltonizerPreferenceUtil.getSecureAccessibilityDaltonizerValue;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -158,14 +161,11 @@
     }
 
     private boolean shouldSeekBarEnabled() {
-        int enabled = Settings.Secure.getInt(
-                mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, 0);
-        int mode = Settings.Secure.getInt(
-                mContentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER, -1);
+        boolean enabled = isSecureAccessibilityDaltonizerEnabled(mContentResolver);
+        int mode = getSecureAccessibilityDaltonizerValue(mContentResolver);
 
-        // enabled == 0 is disabled and also default.
         // mode == 0 is gray scale where saturation level isn't applicable.
         // mode == -1 is disabled and also default.
-        return enabled != 0 && mode != -1 && mode != 0;
+        return enabled && mode != -1 && mode != 0;
     }
 }
diff --git a/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java
index 833638b..818eb5e 100644
--- a/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java
+++ b/src/com/android/settings/accessibility/KeyboardVibrationTogglePreferenceController.java
@@ -110,7 +110,7 @@
     @Override
     public int getAvailabilityStatus() {
         if (mContext.getResources().getBoolean(
-                        com.android.internal.R.bool.config_keyboardVibrationSettingsSupported)) {
+                com.android.internal.R.bool.config_keyboardVibrationSettingsSupported)) {
             return AVAILABLE;
         }
         return UNSUPPORTED_ON_DEVICE;
@@ -128,15 +128,9 @@
         mMetricsFeatureProvider.action(mContext,
                 SettingsEnums.ACTION_KEYBOARD_VIBRATION_CHANGED, isChecked);
         if (success && isChecked) {
-            // Play the preview vibration effect when the toggle is on.
-            final VibrationAttributes touchAttrs =
-                    VibrationPreferenceConfig.createPreviewVibrationAttributes(
-                            VibrationAttributes.USAGE_TOUCH);
-            final VibrationAttributes keyboardAttrs =
-                    new VibrationAttributes.Builder(touchAttrs)
-                            .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
-                            .build();
-            VibrationPreferenceConfig.playVibrationPreview(mVibrator, keyboardAttrs);
+            // Play the preview vibration effect for the IME feedback when the toggle is on.
+            VibrationPreferenceConfig.playVibrationPreview(
+                    mVibrator, VibrationAttributes.USAGE_IME_FEEDBACK);
         }
         return true;
     }
diff --git a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java
index 52f1695..86ddd71 100644
--- a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java
@@ -21,6 +21,7 @@
 import static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled;
 import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
 import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
+import static com.android.settings.accessibility.DaltonizerPreferenceUtil.isSecureAccessibilityDaltonizerEnabled;
 
 import android.app.settings.SettingsEnums;
 import android.content.ComponentName;
@@ -145,7 +146,8 @@
 
     @Override
     protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
-        final boolean isEnabled = Settings.Secure.getInt(getContentResolver(), ENABLED, OFF) == ON;
+        final boolean isEnabled =
+                isSecureAccessibilityDaltonizerEnabled(getContentResolver());
         if (enabled == isEnabled) {
             return;
         }
diff --git a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java
index a304862..ec1fab1 100644
--- a/src/com/android/settings/accessibility/VibrationPreferenceConfig.java
+++ b/src/com/android/settings/accessibility/VibrationPreferenceConfig.java
@@ -68,19 +68,8 @@
     /** Play a vibration effect with intensity just selected by the user. */
     public static void playVibrationPreview(Vibrator vibrator,
             @VibrationAttributes.Usage int vibrationUsage) {
-        playVibrationPreview(vibrator, createPreviewVibrationAttributes(vibrationUsage));
-    }
-
-    /**
-     * Play a vibration effect with intensity just selected by the user.
-     *
-     * @param vibrator The {@link Vibrator} used to play the vibration.
-     * @param vibrationAttributes The {@link VibrationAttributes} to indicate the
-     *        vibration information.
-     */
-    public static void playVibrationPreview(Vibrator vibrator,
-            VibrationAttributes vibrationAttributes) {
-        vibrator.vibrate(PREVIEW_VIBRATION_EFFECT, vibrationAttributes);
+        vibrator.vibrate(PREVIEW_VIBRATION_EFFECT,
+                createPreviewVibrationAttributes(vibrationUsage));
     }
 
     public VibrationPreferenceConfig(Context context, String settingKey,
diff --git a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java
index 1e7ca1e..b40f62f 100644
--- a/src/com/android/settings/applications/appinfo/AppLocaleDetails.java
+++ b/src/com/android/settings/applications/appinfo/AppLocaleDetails.java
@@ -33,6 +33,7 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 
 import com.android.internal.app.LocaleHelper;
@@ -62,6 +63,7 @@
     private LayoutPreference mPrefOfDescription;
     private Preference mPrefOfDisclaimer;
     private ApplicationInfo mApplicationInfo;
+    @Nullable private String mParentLocale;
 
     /**
      * Create a instance of AppLocaleDetails.
@@ -111,6 +113,12 @@
     public void onResume() {
         super.onResume();
         refreshUi();
+        final Activity activity = getActivity();
+        if (mParentLocale != null) {
+            activity.setTitle(mParentLocale);
+        } else {
+            activity.setTitle(R.string.app_locale_picker_title);
+        }
     }
 
     private void refreshUi() {
@@ -215,4 +223,8 @@
             return LocaleHelper.getDisplayName(appLocale.stripExtensions(), appLocale, true);
         }
     }
+
+    public void setParentLocale(@Nullable String localeName) {
+        mParentLocale = localeName;
+    }
 }
diff --git a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
index 0690186..442acd2 100644
--- a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
+++ b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
@@ -101,7 +101,8 @@
         return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
     }
 
-    public void setSliceUri(Uri uri) {
+    /** Sets Slice uri for the preference. */
+    public void setSliceUri(@Nullable Uri uri) {
         mUri = uri;
         mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
             Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
index 4ff7136..398edb6 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
@@ -39,8 +39,8 @@
 
 import com.android.settings.R;
 import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.LocalBluetoothProfile;
 import com.android.settingslib.core.lifecycle.Lifecycle;
 import com.android.settingslib.flags.Flags;
 import com.android.settingslib.utils.ThreadUtils;
@@ -299,57 +299,14 @@
                         + " profiles: "
                         + mCachedDevice.getProfiles());
 
-        AudioDeviceAttributes saDevice = null;
-        for (LocalBluetoothProfile profile : mCachedDevice.getProfiles()) {
-            // pick first enabled profile that is compatible with spatial audio
-            if (SA_PROFILES.contains(profile.getProfileId())
-                    && profile.isEnabled(mCachedDevice.getDevice())) {
-                switch (profile.getProfileId()) {
-                    case BluetoothProfile.A2DP:
-                        saDevice =
-                                new AudioDeviceAttributes(
-                                        AudioDeviceAttributes.ROLE_OUTPUT,
-                                        AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
-                                        mCachedDevice.getAddress());
-                        break;
-                    case BluetoothProfile.LE_AUDIO:
-                        if (mAudioManager.getBluetoothAudioDeviceCategory(
-                                mCachedDevice.getAddress())
-                                == AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) {
-                            saDevice =
-                                    new AudioDeviceAttributes(
-                                            AudioDeviceAttributes.ROLE_OUTPUT,
-                                            AudioDeviceInfo.TYPE_BLE_SPEAKER,
-                                            mCachedDevice.getAddress());
-                        } else {
-                            saDevice =
-                                    new AudioDeviceAttributes(
-                                            AudioDeviceAttributes.ROLE_OUTPUT,
-                                            AudioDeviceInfo.TYPE_BLE_HEADSET,
-                                            mCachedDevice.getAddress());
-                        }
-
-                        break;
-                    case BluetoothProfile.HEARING_AID:
-                        saDevice =
-                                new AudioDeviceAttributes(
-                                        AudioDeviceAttributes.ROLE_OUTPUT,
-                                        AudioDeviceInfo.TYPE_HEARING_AID,
-                                        mCachedDevice.getAddress());
-                        break;
-                    default:
-                        Log.i(
-                                TAG,
-                                "unrecognized profile for spatial audio: "
-                                        + profile.getProfileId());
-                        break;
-                }
-                break;
-            }
-        }
-        mAudioDevice = null;
+        AudioDeviceAttributes saDevice =
+                BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
+                        mCachedDevice,
+                        mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress()));
         if (saDevice != null && mSpatializer.isAvailableForDevice(saDevice)) {
             mAudioDevice = saDevice;
+        } else {
+            mAudioDevice = null;
         }
 
         Log.d(
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index ccf38ed..bd762a1 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -43,10 +43,12 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settings.R;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
 import com.android.settings.connecteddevice.stylus.StylusDevicesController;
 import com.android.settings.core.SettingsUIDeviceConfig;
 import com.android.settings.dashboard.RestrictedDashboardFragment;
@@ -60,9 +62,11 @@
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
     public static final String KEY_DEVICE_ADDRESS = "device_address";
@@ -98,6 +102,8 @@
     @VisibleForTesting
     CachedBluetoothDevice mCachedDevice;
     BluetoothAdapter mBluetoothAdapter;
+    @VisibleForTesting
+    DeviceDetailsFragmentFormatter mFormatter;
 
     @Nullable
     InputDevice mInputDevice;
@@ -214,18 +220,29 @@
             finish();
             return;
         }
-        use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice, this);
-        use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager, this);
-        use(KeyboardSettingsPreferenceController.class).init(mCachedDevice);
+        getController(
+                AdvancedBluetoothDetailsHeaderController.class,
+                controller -> controller.init(mCachedDevice, this));
+        getController(
+                LeAudioBluetoothDetailsHeaderController.class,
+                controller -> controller.init(mCachedDevice, mManager, this));
+        getController(
+                KeyboardSettingsPreferenceController.class,
+                controller -> controller.init(mCachedDevice));
 
         final BluetoothFeatureProvider featureProvider =
                 FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider();
         final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
                 SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
 
-        use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
-                ? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
-                : null);
+        getController(
+                BlockingPrefWithSliceController.class,
+                controller ->
+                        controller.setSliceUri(
+                                sliceEnabled
+                                        ? featureProvider.getBluetoothDeviceSettingsUri(
+                                                mCachedDevice.getDevice())
+                                        : null));
 
         mManager.getEventManager().registerCallback(mBluetoothCallback);
         mBluetoothAdapter.addOnMetadataChangedListener(
@@ -257,21 +274,35 @@
             }
         }
         mExtraControlUriLoaded |= controlUri != null;
-        final SlicePreferenceController slicePreferenceController = use(
-                SlicePreferenceController.class);
-        slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
-        slicePreferenceController.onStart();
-        slicePreferenceController.displayPreference(getPreferenceScreen());
+
+        Uri finalControlUri = controlUri;
+        getController(SlicePreferenceController.class, controller -> {
+            controller.setSliceUri(sliceEnabled ? finalControlUri : null);
+            controller.onStart();
+            controller.displayPreference(getPreferenceScreen());
+        });
+
 
         // Temporarily fix the issue that the page will be automatically scrolled to a wrong
         // position when entering the page. This will make sure the bluetooth header is shown on top
         // of the page.
-        use(LeAudioBluetoothDetailsHeaderController.class).displayPreference(
-                getPreferenceScreen());
-        use(AdvancedBluetoothDetailsHeaderController.class).displayPreference(
-                getPreferenceScreen());
-        use(BluetoothDetailsHeaderController.class).displayPreference(
-                getPreferenceScreen());
+        getController(
+                LeAudioBluetoothDetailsHeaderController.class,
+                controller -> controller.displayPreference(getPreferenceScreen()));
+        getController(
+                AdvancedBluetoothDetailsHeaderController.class,
+                controller -> controller.displayPreference(getPreferenceScreen()));
+        getController(
+                BluetoothDetailsHeaderController.class,
+                controller -> controller.displayPreference(getPreferenceScreen()));
+    }
+
+    protected <T extends AbstractPreferenceController> void getController(Class<T> clazz,
+            Consumer<T> action) {
+        T controller = use(clazz);
+        if (controller != null) {
+            action.accept(controller);
+        }
     }
 
     private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
@@ -309,6 +340,14 @@
     }
 
     @Override
+    public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) {
+        super.onCreatePreferences(savedInstanceState, rootKey);
+        if (Flags.enableBluetoothDeviceDetailsPolish()) {
+            mFormatter.updateLayout();
+        }
+    }
+
+    @Override
     public void onResume() {
         super.onResume();
         finishFragmentIfNecessary();
@@ -359,7 +398,29 @@
     }
 
     @Override
+    protected void addPreferenceController(AbstractPreferenceController controller) {
+        if (Flags.enableBluetoothDeviceDetailsPolish()) {
+            List<String> keys = mFormatter.getVisiblePreferenceKeysForMainPage();
+            Lifecycle lifecycle = getSettingsLifecycle();
+            if (keys == null || keys.contains(controller.getPreferenceKey())) {
+                super.addPreferenceController(controller);
+            } else if (controller instanceof LifecycleObserver) {
+                lifecycle.removeObserver((LifecycleObserver) controller);
+            }
+        } else {
+            super.addPreferenceController(controller);
+        }
+    }
+
+    @Override
     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+        if (Flags.enableBluetoothDeviceDetailsPolish()) {
+            mFormatter =
+                    FeatureFactory.getFeatureFactory()
+                            .getBluetoothFeatureProvider()
+                            .getDeviceDetailsFragmentFormatter(
+                                    requireContext(), this, mBluetoothAdapter, mCachedDevice);
+        }
         ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
 
         if (mCachedDevice != null) {
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
index 1751082..be0f6f3 100644
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
@@ -16,15 +16,23 @@
 
 package com.android.settings.bluetooth;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
 import android.content.Context;
+import android.media.AudioManager;
 import android.media.Spatializer;
 import android.net.Uri;
 
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleCoroutineScope;
 import androidx.preference.Preference;
 
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
 
 import java.util.List;
 import java.util.Set;
@@ -84,4 +92,26 @@
      */
     Set<String> getInvisibleProfilePreferenceKeys(
             Context context, BluetoothDevice bluetoothDevice);
+
+    /** Gets DeviceSettingRepository. */
+    @NonNull
+    DeviceSettingRepository getDeviceSettingRepository(
+            @NonNull Context context,
+            @NonNull BluetoothAdapter bluetoothAdapter,
+            @NonNull LifecycleCoroutineScope scope);
+
+    /** Gets spatial audio interactor. */
+    @NonNull
+    SpatialAudioInteractor getSpatialAudioInteractor(
+            @NonNull Context context,
+            @NonNull AudioManager audioManager,
+            @NonNull LifecycleCoroutineScope scope);
+
+    /** Gets device details fragment layout formatter. */
+    @NonNull
+    DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
+            @NonNull Context context,
+            @NonNull SettingsPreferenceFragment fragment,
+            @NonNull BluetoothAdapter bluetoothAdapter,
+            @NonNull CachedBluetoothDevice cachedDevice);
 }
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java
deleted file mode 100644
index 2d4ac49..0000000
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.bluetooth;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.AudioManager;
-import android.media.Spatializer;
-import android.net.Uri;
-
-import androidx.preference.Preference;
-
-import com.android.settingslib.bluetooth.BluetoothUtils;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * Impl of {@link BluetoothFeatureProvider}
- */
-public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
-
-    @Override
-    public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) {
-        final byte[] uriByte = bluetoothDevice.getMetadata(
-                BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI);
-        return uriByte == null ? null : Uri.parse(new String(uriByte));
-    }
-
-    @Override
-    public String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice) {
-        return BluetoothUtils.getControlUriMetaData(bluetoothDevice);
-    }
-
-    @Override
-    public List<ComponentName> getRelatedTools() {
-        return null;
-    }
-
-    @Override
-    public Spatializer getSpatializer(Context context) {
-        AudioManager audioManager = context.getSystemService(AudioManager.class);
-        return audioManager.getSpatializer();
-    }
-
-    @Override
-    public List<Preference> getBluetoothExtraOptions(Context context,
-            CachedBluetoothDevice device) {
-        return ImmutableList.of();
-    }
-
-    @Override
-    public Set<String> getInvisibleProfilePreferenceKeys(
-            Context context, BluetoothDevice bluetoothDevice) {
-        return ImmutableSet.of();
-    }
-}
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt
new file mode 100644
index 0000000..3a549c6
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.ComponentName
+import android.content.Context
+import android.media.AudioManager
+import android.media.Spatializer
+import android.net.Uri
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.preference.Preference
+import com.android.settings.SettingsPreferenceFragment
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
+import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
+import com.android.settingslib.media.domain.interactor.SpatializerInteractor
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableSet
+import kotlinx.coroutines.Dispatchers
+
+/** Impl of [BluetoothFeatureProvider] */
+open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
+    override fun getBluetoothDeviceSettingsUri(bluetoothDevice: BluetoothDevice): Uri? {
+        val uriByte = bluetoothDevice.getMetadata(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI)
+        return uriByte?.let { Uri.parse(String(it)) }
+    }
+
+    override fun getBluetoothDeviceControlUri(bluetoothDevice: BluetoothDevice): String? {
+        return BluetoothUtils.getControlUriMetaData(bluetoothDevice)
+    }
+
+    override fun getRelatedTools(): List<ComponentName>? {
+        return null
+    }
+
+    override fun getSpatializer(context: Context): Spatializer? {
+        val audioManager = context.getSystemService(AudioManager::class.java)
+        return audioManager.spatializer
+    }
+
+    override fun getBluetoothExtraOptions(
+        context: Context,
+        device: CachedBluetoothDevice
+    ): List<Preference>? {
+        return ImmutableList.of<Preference>()
+    }
+
+    override fun getInvisibleProfilePreferenceKeys(
+        context: Context,
+        bluetoothDevice: BluetoothDevice
+    ): Set<String> {
+        return ImmutableSet.of()
+    }
+
+    override fun getDeviceSettingRepository(
+        context: Context,
+        bluetoothAdapter: BluetoothAdapter,
+        scope: LifecycleCoroutineScope
+    ): DeviceSettingRepository =
+        DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
+
+    override fun getSpatialAudioInteractor(
+        context: Context,
+        audioManager: AudioManager,
+        scope: LifecycleCoroutineScope
+    ): SpatialAudioInteractor {
+        return SpatialAudioInteractorImpl(
+            context, audioManager,
+            SpatializerInteractor(
+                SpatializerRepositoryImpl(
+                    audioManager.spatializer,
+                    Dispatchers.IO
+                )
+            ), scope, Dispatchers.IO)
+    }
+
+    override fun getDeviceDetailsFragmentFormatter(
+        context: Context,
+        fragment: SettingsPreferenceFragment,
+        bluetoothAdapter: BluetoothAdapter,
+        cachedDevice: CachedBluetoothDevice
+    ): DeviceDetailsFragmentFormatter {
+        return DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
+    }
+}
diff --git a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt
new file mode 100644
index 0000000..6b72b53
--- /dev/null
+++ b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.domain.interactor
+
+import android.content.Context
+import android.media.AudioManager
+import android.util.Log
+import com.android.settings.R
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
+import com.android.settingslib.media.domain.interactor.SpatializerInteractor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Provides device setting for spatial audio. */
+interface SpatialAudioInteractor {
+    /** Gets device setting for spatial audio */
+    fun getDeviceSetting(
+        cachedDevice: CachedBluetoothDevice,
+    ): Flow<DeviceSettingModel?>
+}
+
+class SpatialAudioInteractorImpl(
+    private val context: Context,
+    private val audioManager: AudioManager,
+    private val spatializerInteractor: SpatializerInteractor,
+    private val coroutineScope: CoroutineScope,
+    private val backgroundCoroutineContext: CoroutineContext,
+) : SpatialAudioInteractor {
+    private val spatialAudioOffToggle =
+        ToggleModel(
+            context.getString(R.string.spatial_audio_multi_toggle_off),
+            DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off))
+    private val spatialAudioOnToggle =
+        ToggleModel(
+            context.getString(R.string.spatial_audio_multi_toggle_on),
+            DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio))
+    private val headTrackingOnToggle =
+        ToggleModel(
+            context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on),
+            DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking))
+    private val changes = MutableSharedFlow<Unit>()
+
+    override fun getDeviceSetting(
+        cachedDevice: CachedBluetoothDevice,
+    ): Flow<DeviceSettingModel?> =
+        changes
+            .onStart { emit(Unit) }
+            .map { getSpatialAudioDeviceSettingModel(cachedDevice) }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null)
+
+    private suspend fun getSpatialAudioDeviceSettingModel(
+        cachedDevice: CachedBluetoothDevice,
+    ): DeviceSettingModel? {
+        // TODO(b/343317785): use audio repository instead of calling AudioManager directly.
+        Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}")
+        val attributes =
+            BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
+                cachedDevice, audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address))
+                ?: run {
+                    Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.")
+                    return null
+                }
+
+        Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.")
+        val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes)
+        if (!spatialAudioAvailable) {
+            Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}")
+            return null
+        }
+        val headTrackingAvailable =
+            spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes)
+        val toggles =
+            if (headTrackingAvailable) {
+                listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle)
+            } else {
+                listOf(spatialAudioOffToggle, spatialAudioOnToggle)
+            }
+        val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes)
+        val headTrackingEnabled =
+            spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes)
+
+        val activeIndex =
+            when {
+                headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED
+                spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON
+                else -> INDEX_SPATIAL_AUDIO_OFF
+            }
+        Log.i(
+            TAG,
+            "Head tracking available: $headTrackingAvailable, " +
+                "spatial audio enabled: $spatialAudioEnabled, " +
+                "head tracking enabled: $headTrackingEnabled")
+        return DeviceSettingModel.MultiTogglePreference(
+            cachedDevice = cachedDevice,
+            id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE,
+            title = context.getString(R.string.spatial_audio_multi_toggle_title),
+            toggles = toggles,
+            isActive = spatialAudioEnabled,
+            state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex),
+            isAllowedChangingState = true,
+            updateState = { newState ->
+                coroutineScope.launch(backgroundCoroutineContext) {
+                    Log.i(TAG, "Update spatial audio state: $newState")
+                    when (newState.selectedIndex) {
+                        INDEX_SPATIAL_AUDIO_OFF -> {
+                            spatializerInteractor.setSpatialAudioEnabled(attributes, false)
+                        }
+                        INDEX_SPATIAL_AUDIO_ON -> {
+                            spatializerInteractor.setSpatialAudioEnabled(attributes, true)
+                            spatializerInteractor.setHeadTrackingEnabled(attributes, false)
+                        }
+                        INDEX_HEAD_TRACKING_ENABLED -> {
+                            spatializerInteractor.setSpatialAudioEnabled(attributes, true)
+                            spatializerInteractor.setHeadTrackingEnabled(attributes, true)
+                        }
+                    }
+                    changes.emit(Unit)
+                }
+            })
+    }
+
+    companion object {
+        private const val TAG = "SpatialAudioInteractorImpl"
+        private const val INDEX_SPATIAL_AUDIO_OFF = 0
+        private const val INDEX_SPATIAL_AUDIO_ON = 1
+        private const val INDEX_HEAD_TRACKING_ENABLED = 2
+    }
+}
diff --git a/src/com/android/settings/bluetooth/ui/composable/Icon.kt b/src/com/android/settings/bluetooth/ui/composable/Icon.kt
new file mode 100644
index 0000000..676bd14
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/composable/Icon.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.composable
+
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
+
+@Composable
+fun Icon(
+    icon: DeviceSettingIcon,
+    modifier: Modifier = Modifier,
+    tint: Color = LocalContentColor.current,
+) {
+    when (icon) {
+        is DeviceSettingIcon.BitmapIcon ->
+            androidx.compose.material3.Icon(
+                icon.bitmap.asImageBitmap(),
+                contentDescription = null,
+                modifier = modifier,
+                tint = LocalContentColor.current)
+        is DeviceSettingIcon.ResourceIcon ->
+            androidx.compose.material3.Icon(
+                painterResource(icon.resId),
+                contentDescription = null,
+                modifier = modifier,
+                tint = tint)
+        else -> {}
+    }
+}
diff --git a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
similarity index 80%
rename from src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt
rename to src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
index e4ca00d..8fe3c25 100644
--- a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt
+++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settings.bluetooth.ui
+package com.android.settings.bluetooth.ui.composable
 
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.background
@@ -51,7 +51,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.layout.boundsInParent
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
@@ -67,6 +66,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.window.DialogProperties
 import com.android.settings.R
+import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
 import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -97,35 +97,29 @@
                     Surface(
                         modifier = Modifier.height(64.dp),
                         shape = RoundedCornerShape(28.dp),
-                        color = MaterialTheme.colorScheme.surface
-                    ) {
-                        Button(
-                            modifier =
-                                Modifier.fillMaxSize().padding(8.dp).semantics {
-                                    role = Role.Switch
-                                    toggleableState =
-                                        if (preferenceModel.isActive) {
-                                            ToggleableState.On
-                                        } else {
-                                            ToggleableState.Off
-                                        }
-                                    contentDescription = preferenceModel.title
-                                },
-                            onClick = { settingIdForPopUp = preferenceModel.id },
-                            shape = RoundedCornerShape(20.dp),
-                            colors = getButtonColors(preferenceModel.isActive),
-                            contentPadding = PaddingValues(0.dp)
-                        ) {
-                            Icon(
-                                preferenceModel.toggles[preferenceModel.state.selectedIndex]
-                                    .icon
-                                    .asImageBitmap(),
-                                contentDescription = null,
-                                modifier = Modifier.size(24.dp),
-                                tint = LocalContentColor.current
-                            )
+                        color = MaterialTheme.colorScheme.surface) {
+                            Button(
+                                modifier =
+                                    Modifier.fillMaxSize().padding(8.dp).semantics {
+                                        role = Role.Switch
+                                        toggleableState =
+                                            if (preferenceModel.isActive) {
+                                                ToggleableState.On
+                                            } else {
+                                                ToggleableState.Off
+                                            }
+                                        contentDescription = preferenceModel.title
+                                    },
+                                onClick = { settingIdForPopUp = preferenceModel.id },
+                                shape = RoundedCornerShape(20.dp),
+                                colors = getButtonColors(preferenceModel.isActive),
+                                contentPadding = PaddingValues(0.dp)) {
+                                    DeviceSettingComposeIcon(
+                                        preferenceModel.toggles[preferenceModel.state.selectedIndex]
+                                            .icon,
+                                        modifier = Modifier.size(24.dp))
+                                }
                         }
-                    }
                 }
                 Row { Text(text = preferenceModel.title, fontSize = 12.sp) }
             }
@@ -173,8 +167,7 @@
                             Icon(
                                 painterResource(id = R.drawable.ic_close),
                                 null,
-                                tint = MaterialTheme.colorScheme.inverseSurface
-                            )
+                                tint = MaterialTheme.colorScheme.inverseSurface)
                         }
                         Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
                             dialogContent(multiTogglePreference)
@@ -182,8 +175,7 @@
                     }
                 },
             )
-        }
-    )
+        })
 }
 
 @Composable
@@ -208,9 +200,7 @@
                 Modifier.fillMaxWidth()
                     .height(64.dp)
                     .background(
-                        MaterialTheme.colorScheme.surface,
-                        shape = RoundedCornerShape(28.dp)
-                    ),
+                        MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)),
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.SpaceEvenly,
         ) {
@@ -224,9 +214,7 @@
                                     .width(selectedRect!!.width.toDp())
                                     .background(
                                         MaterialTheme.colorScheme.tertiaryContainer,
-                                        shape = RoundedCornerShape(20.dp)
-                                    )
-                        )
+                                        shape = RoundedCornerShape(20.dp)))
                     }
                 }
                 Row {
@@ -238,9 +226,7 @@
                                     .padding(horizontal = 8.dp)
                                     .height(48.dp)
                                     .background(
-                                        Color.Transparent,
-                                        shape = RoundedCornerShape(28.dp)
-                                    )
+                                        Color.Transparent, shape = RoundedCornerShape(28.dp))
                                     .onGloballyPositioned { layoutCoordinates ->
                                         if (selected) {
                                             selectedRect = layoutCoordinates.boundsInParent()
@@ -252,22 +238,16 @@
                             Button(
                                 onClick = {
                                     multiTogglePreference.updateState(
-                                        DeviceSettingStateModel.MultiTogglePreferenceState(idx)
-                                    )
+                                        DeviceSettingStateModel.MultiTogglePreferenceState(idx))
                                 },
                                 modifier = Modifier.fillMaxSize(),
                                 colors =
                                     ButtonDefaults.buttonColors(
                                         containerColor = Color.Transparent,
-                                        contentColor = LocalContentColor.current
-                                    ),
+                                        contentColor = LocalContentColor.current),
                             ) {
-                                Icon(
-                                    bitmap = toggle.icon.asImageBitmap(),
-                                    null,
-                                    modifier = Modifier.size(24.dp),
-                                    tint = LocalContentColor.current
-                                )
+                                DeviceSettingComposeIcon(
+                                    toggle.icon, modifier = Modifier.size(24.dp))
                             }
                         }
                     }
@@ -285,8 +265,7 @@
                     text = toggle.label,
                     fontSize = 12.sp,
                     textAlign = TextAlign.Center,
-                    modifier = Modifier.weight(1f).padding(horizontal = 8.dp)
-                )
+                    modifier = Modifier.weight(1f).padding(horizontal = 8.dp))
             }
         }
     }
diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
new file mode 100644
index 0000000..87e2e8b
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.layout
+
+import kotlinx.coroutines.flow.Flow
+
+/** Represent the layout of device settings. */
+data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>)
+
+/** Represent a row in the layout. */
+data class DeviceSettingLayoutRow(val settingIds: Flow<List<Int>>)
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
new file mode 100644
index 0000000..b75579d
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.view
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.media.AudioManager
+import android.util.Log
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import com.android.settings.SettingsPreferenceFragment
+import com.android.settings.bluetooth.ui.composable.Icon
+import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settings.spa.preference.ComposePreference
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.SwitchPreference
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
+
+
+/** Handles device details fragment layout according to config. */
+interface DeviceDetailsFragmentFormatter {
+    /** Gets keys of visible preferences in built-in preference in xml. */
+    fun getVisiblePreferenceKeysForMainPage(): List<String>?
+
+    /** Updates device details fragment layout. */
+    fun updateLayout()
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceDetailsFragmentFormatterImpl(
+    private val context: Context,
+    private val fragment: SettingsPreferenceFragment,
+    bluetoothAdapter: BluetoothAdapter,
+    private val cachedDevice: CachedBluetoothDevice
+) : DeviceDetailsFragmentFormatter {
+    private val repository =
+        featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+            context, bluetoothAdapter, fragment.lifecycleScope)
+    private val spatialAudioInteractor =
+        featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
+            context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope)
+    private val viewModel: BluetoothDeviceDetailsViewModel =
+        ViewModelProvider(
+                fragment,
+                BluetoothDeviceDetailsViewModel.Factory(
+                    repository,
+                    spatialAudioInteractor,
+                    cachedDevice,
+                ))
+            .get(BluetoothDeviceDetailsViewModel::class.java)
+
+    override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
+        viewModel
+            .getItems()
+            ?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
+            ?.mapNotNull { it.preferenceKey }
+    }
+
+    /** Updates bluetooth device details fragment layout. */
+    override fun updateLayout() = runBlocking {
+        val items = viewModel.getItems() ?: return@runBlocking
+        val layout = viewModel.getLayout() ?: return@runBlocking
+        val prefKeyToSettingId =
+            items
+                .filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
+                .associateBy({ it.preferenceKey }, { it.settingId })
+
+        val settingIdToXmlPreferences: MutableMap<Int, Preference> = HashMap()
+        for (i in 0 until fragment.preferenceScreen.preferenceCount) {
+            val pref = fragment.preferenceScreen.getPreference(i)
+            prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
+        }
+        fragment.preferenceScreen.removeAll()
+
+        for (row in items.indices) {
+            val settingId = items[row].settingId
+            if (settingIdToXmlPreferences.containsKey(settingId)) {
+                fragment.preferenceScreen.addPreference(
+                    settingIdToXmlPreferences[settingId]!!.apply { order = row })
+            } else {
+                val pref =
+                    ComposePreference(context)
+                        .apply {
+                            key = getPreferenceKey(settingId)
+                            order = row
+                        }
+                        .also { pref -> pref.setContent { buildPreference(layout, row) } }
+                fragment.preferenceScreen.addPreference(pref)
+            }
+        }
+    }
+
+    @Composable
+    private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
+        val contents by
+            remember(row) {
+                    layout.rows[row].settingIds.flatMapLatest { settingIds ->
+                        if (settingIds.isEmpty()) {
+                            flowOf(emptyList<DeviceSettingModel>())
+                        } else {
+                            combine(
+                                settingIds.map { settingId ->
+                                    viewModel.getDeviceSetting(cachedDevice, settingId)
+                                }) {
+                                    it.toList()
+                                }
+                        }
+                    }
+                }
+                .collectAsStateWithLifecycle(initialValue = listOf())
+
+        val settings = contents
+        when (settings.size) {
+            0 -> {}
+            1 -> {
+                when (val setting = settings[0]) {
+                    is DeviceSettingModel.ActionSwitchPreference -> {
+                        buildActionSwitchPreference(setting)
+                    }
+                    is DeviceSettingModel.MultiTogglePreference -> {
+                        buildMultiTogglePreference(listOf(setting))
+                    }
+                    null -> {}
+                    else -> {
+                        Log.w(TAG, "Unknown preference type ${setting.id}, skip.")
+                    }
+                }
+            }
+            else -> {
+                if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) {
+                    return
+                }
+                buildMultiTogglePreference(
+                    settings.filterIsInstance<DeviceSettingModel.MultiTogglePreference>())
+            }
+        }
+    }
+
+    @Composable
+    private fun buildMultiTogglePreference(prefs: List<DeviceSettingModel.MultiTogglePreference>) {
+        MultiTogglePreferenceGroup(prefs)
+    }
+
+    @Composable
+    private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) {
+        if (model.switchState != null) {
+            val switchPrefModel =
+                object : SwitchPreferenceModel {
+                    override val title = model.title
+                    override val summary = { model.summary ?: "" }
+                    override val checked = { model.switchState?.checked }
+                    override val onCheckedChange = { newChecked: Boolean ->
+                        model.updateState?.invoke(
+                            DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked))
+                        Unit
+                    }
+                    override val icon = @Composable { deviceSettingIcon(model) }
+                }
+            if (model.intent != null) {
+                TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) }
+            } else {
+                SwitchPreference(switchPrefModel)
+            }
+        } else {
+            SpaPreference(
+                object : PreferenceModel {
+                    override val title = model.title
+                    override val summary = { model.summary ?: "" }
+                    override val onClick = {
+                        model.intent?.let { context.startActivity(it) }
+                        Unit
+                    }
+                    override val icon = @Composable { deviceSettingIcon(model) }
+                })
+        }
+    }
+
+    @Composable
+    private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
+        model.icon?.let { icon ->
+            Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize))
+        }
+    }
+
+    private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
+
+    companion object {
+        const val TAG = "DeviceDetailsFormatter"
+    }
+}
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
new file mode 100644
index 0000000..befff83
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class BluetoothDeviceDetailsViewModel(
+    private val deviceSettingRepository: DeviceSettingRepository,
+    private val spatialAudioInteractor: SpatialAudioInteractor,
+    private val cachedDevice: CachedBluetoothDevice,
+) : ViewModel() {
+    private val items =
+        viewModelScope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
+            deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
+        }
+
+    suspend fun getItems(): List<DeviceSettingConfigItemModel>? = items.await()?.mainItems
+
+    fun getDeviceSetting(
+        cachedDevice: CachedBluetoothDevice,
+        @DeviceSettingId settingId: Int
+    ): Flow<DeviceSettingModel?> {
+        return when (settingId) {
+            DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE ->
+                spatialAudioInteractor.getDeviceSetting(cachedDevice)
+            else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
+        }
+    }
+
+    suspend fun getLayout(): DeviceSettingLayout? {
+        val configItems = getItems() ?: return null
+        val idToDeviceSetting =
+            configItems
+                .filterIsInstance<DeviceSettingConfigItemModel.AppProvidedItem>()
+                .associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
+
+        val configDeviceSetting =
+            configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
+        val positionToSettingIds =
+            combine(configDeviceSetting) { settings ->
+                    val positionMapping = mutableMapOf<Int, List<Int>>()
+                    var multiToggleSettingIds: MutableList<Int>? = null
+                    for (i in settings.indices) {
+                        val configItem = configItems[i]
+                        val setting = settings[i]
+                        val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem
+                        if (!isXmlPreference && setting == null) {
+                            continue
+                        }
+                        if (setting !is DeviceSettingModel.MultiTogglePreference) {
+                            multiToggleSettingIds = null
+                            positionMapping[i] = listOf(configItem.settingId)
+                            continue
+                        }
+
+                        if (multiToggleSettingIds != null) {
+                            multiToggleSettingIds.add(setting.id)
+                        } else {
+                            multiToggleSettingIds = mutableListOf(setting.id)
+                            positionMapping[i] = multiToggleSettingIds
+                        }
+                    }
+                    positionMapping
+                }
+                .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+        return DeviceSettingLayout(
+            configItems.indices.map { idx ->
+                DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
+            })
+    }
+
+    class Factory(
+        private val deviceSettingRepository: DeviceSettingRepository,
+        private val spatialAudioInteractor: SpatialAudioInteractor,
+        private val cachedDevice: CachedBluetoothDevice,
+    ) : ViewModelProvider.Factory {
+        override fun <T : ViewModel> create(modelClass: Class<T>): T {
+            @Suppress("UNCHECKED_CAST")
+            return BluetoothDeviceDetailsViewModel(
+                deviceSettingRepository, spatialAudioInteractor, cachedDevice)
+                as T
+        }
+    }
+
+    companion object {
+        private const val TAG = "BluetoothDeviceDetailsViewModel"
+    }
+}
diff --git a/src/com/android/settings/localepicker/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
index b284c8d..2294b9b 100644
--- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java
+++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java
@@ -37,6 +37,7 @@
 import android.widget.FrameLayout;
 import android.widget.ListView;
 
+import androidx.annotation.Nullable;
 import androidx.core.app.NotificationCompat;
 import androidx.core.view.ViewCompat;
 
@@ -67,6 +68,7 @@
     private View mAppLocaleDetailContainer;
     private NotificationController mNotificationController;
     private MetricsFeatureProvider mMetricsFeatureProvider;
+    @Nullable private String mParentLocale;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -130,6 +132,11 @@
     }
 
     @Override
+    public void onParentLocaleSelected(LocaleStore.LocaleInfo localeInfo) {
+        mParentLocale = localeInfo.getFullNameNative();
+    }
+
+    @Override
     public boolean onMenuItemActionCollapse(MenuItem item) {
         mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/);
         ViewCompat.setNestedScrollingEnabled(mAppLocaleDetails.getListView(), true);
@@ -258,6 +265,12 @@
                         super.onFragmentViewCreated(fm, f, v, s);
                         ListView listView = (ListView) v.findViewById(android.R.id.list);
                         if (listView != null) {
+                            if (mParentLocale != null) {
+                                mAppLocaleDetails = AppLocaleDetails.newInstance(mPackageName,
+                                        getUserId());
+                                mAppLocaleDetailContainer = launchAppLocaleDetailsPage();
+                                mAppLocaleDetails.setParentLocale(mParentLocale);
+                            }
                             listView.addHeaderView(mAppLocaleDetailContainer);
                         }
                     }
diff --git a/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java
index 6e596e1..05cb6a4 100644
--- a/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java
+++ b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java
@@ -39,6 +39,7 @@
         implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener {
     private static final String TAG = LocalePickerWithRegionActivity.class.getSimpleName();
     private static final String PARENT_FRAGMENT_NAME = "localeListEditor";
+    private static final String CHILD_FRAGMENT_NAME = "LocalePickerWithRegion";
 
     private LocalePickerWithRegion mSelector;
 
@@ -68,12 +69,15 @@
                 explicitLocales,
                 null /* appPackageName */,
                 this);
-        getFragmentManager()
-                .beginTransaction()
-                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
-                .replace(R.id.content_frame, mSelector)
-                .addToBackStack(PARENT_FRAGMENT_NAME)
-                .commit();
+
+        if (getFragmentManager().findFragmentByTag(CHILD_FRAGMENT_NAME) == null) {
+            getFragmentManager()
+                    .beginTransaction()
+                    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+                    .replace(R.id.content_frame, mSelector, CHILD_FRAGMENT_NAME)
+                    .addToBackStack(PARENT_FRAGMENT_NAME)
+                    .commit();
+        }
     }
 
     @Override
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index 0fcfcb5..e240682 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -1068,6 +1068,10 @@
     @VisibleForTesting
     void launchNetworkDetailsFragment(LongPressWifiEntryPreference pref) {
         final WifiEntry wifiEntry = pref.getWifiEntry();
+        if (!wifiEntry.isSaved()) {
+            Log.w(TAG, "launchNetworkDetailsFragment: Don't launch because WifiEntry isn't saved!");
+            return;
+        }
         final Context context = requireContext();
 
         final Bundle bundle = new Bundle();
diff --git a/src/com/android/settings/network/telephony/NetworkSelectRepository.kt b/src/com/android/settings/network/telephony/NetworkSelectRepository.kt
index 1f5fbc2..d95c90e 100644
--- a/src/com/android/settings/network/telephony/NetworkSelectRepository.kt
+++ b/src/com/android/settings/network/telephony/NetworkSelectRepository.kt
@@ -18,8 +18,10 @@
 
 import android.content.Context
 import android.telephony.AccessNetworkConstants
+import android.telephony.CarrierConfigManager
 import android.telephony.NetworkRegistrationInfo
 import android.telephony.TelephonyManager
+import android.telephony.satellite.SatelliteManager
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
@@ -28,9 +30,11 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-class NetworkSelectRepository(context: Context, subId: Int) {
+class NetworkSelectRepository(context: Context, private val subId: Int) {
     private val telephonyManager =
         context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId)
+    private val satelliteManager = context.getSystemService(SatelliteManager::class.java)
+    private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)
 
     data class NetworkRegistrationAndForbiddenInfo(
         val networkList: List<NetworkRegistrationInfo>,
@@ -55,10 +59,21 @@
         if (telephonyManager.dataState != TelephonyManager.DATA_CONNECTED) return null
         // Try to get the network registration states
         val serviceState = telephonyManager.serviceState ?: return null
-        val networkList = serviceState.getNetworkRegistrationInfoListForTransportType(
+        var networkList = serviceState.getNetworkRegistrationInfoListForTransportType(
             AccessNetworkConstants.TRANSPORT_TYPE_WWAN
         )
         if (networkList.isEmpty()) return null
+
+        val satellitePlmn = getSatellitePlmns()
+        // If connected network is Satellite, filter out
+        if (satellitePlmn.isNotEmpty()) {
+            val filteredNetworkList = networkList.filter {
+                val cellIdentity = it.cellIdentity
+                val plmn = cellIdentity?.plmn
+                plmn != null && !satellitePlmn.contains(plmn)
+            }
+            networkList = filteredNetworkList
+        }
         // Due to the aggregation of cell between carriers, it's possible to get CellIdentity
         // containing forbidden PLMN.
         // Getting current network from ServiceState is no longer a good idea.
@@ -72,4 +87,24 @@
     private fun getForbiddenPlmns(): List<String> {
         return telephonyManager.forbiddenPlmns?.toList() ?: emptyList()
     }
+
+    /**
+     * Update satellite PLMNs from the satellite framework.
+     */
+    private fun getSatellitePlmns(): List<String> {
+        val config = carrierConfigManager.getConfigForSubId(
+            subId,
+            CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL
+        )
+
+        val shouldFilter = config.getBoolean(
+            CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL,
+            true)
+
+        return if (shouldFilter) {
+            satelliteManager.getSatellitePlmnsForCarrier(subId)
+        } else {
+            emptyList();
+        }
+    }
 }
diff --git a/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
index 200a47b..778c788 100644
--- a/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
+++ b/src/com/android/settings/notification/app/AppChannelsBypassingDndPreferenceController.java
@@ -226,7 +226,10 @@
                         .setArguments(channelArgs)
                         .setUserHandle(UserHandle.of(mAppRow.userId))
                         .setTitleRes(com.android.settings.R.string.notification_channel_title)
-                        .setSourceMetricsCategory(SettingsEnums.DND_APPS_BYPASSING)
+                        .setSourceMetricsCategory(
+                                android.app.Flags.modesUi()
+                                    ? SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP_CHANNELS
+                                        : SettingsEnums.DND_APPS_BYPASSING)
                         .launch();
                 return true;
             });
diff --git a/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java b/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java
index 4fab7e2..b5e2b13 100644
--- a/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java
+++ b/src/com/android/settings/notification/app/AppChannelsBypassingDndSettings.java
@@ -40,7 +40,9 @@
 
     @Override
     public int getMetricsCategory() {
-        return SettingsEnums.DND_APPS_BYPASSING;
+        return android.app.Flags.modesUi()
+                ? SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP_CHANNELS
+                : SettingsEnums.DND_APPS_BYPASSING;
     }
 
     @Override
diff --git a/src/com/android/settings/notification/modes/CircularIconsPreference.java b/src/com/android/settings/notification/modes/CircularIconsPreference.java
index 0766ccd..ccf7f52 100644
--- a/src/com/android/settings/notification/modes/CircularIconsPreference.java
+++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java
@@ -49,7 +49,9 @@
 
     private static final float DISABLED_ITEM_ALPHA = 0.3f;
 
-    record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) { }
+    record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) {
+        static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0);
+    }
 
     private Executor mUiExecutor;
 
@@ -126,6 +128,7 @@
             // We know what icons we want, but haven't yet loaded them.
             if (mIconSet.size() == 0) {
                 container.setVisibility(View.GONE);
+                mLoadedIcons = LoadedIcons.EMPTY;
                 return;
             }
             container.setVisibility(View.VISIBLE);
@@ -137,7 +140,7 @@
                             @Override
                             public void onGlobalLayout() {
                                 container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                                startLoadingIcons(container, mIconSet);
+                                notifyChanged();
                             }
                         }
                 );
diff --git a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java
index f26de76..830baaf 100644
--- a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java
+++ b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java
@@ -25,6 +25,7 @@
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.app.ActionBar;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
@@ -164,7 +165,8 @@
             // they happen to go back. Forward the activity result in case we got here (indirectly)
             // from some app that is waiting for the result.
             if (updated) {
-                ZenSubSettingLauncher.forMode(this, modeId)
+                ZenSubSettingLauncher.forModeFragment(this, ZenModeFragment.class, modeId,
+                                SettingsEnums.ZEN_MODE_INTERSTITIAL)
                         .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT).launch();
             }
             finish();
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java
index 19035dd..ec72c83 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java
@@ -47,7 +47,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY;
+        return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
index 1521a8b..45287ab 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
@@ -20,7 +20,10 @@
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.app.Application;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -48,6 +51,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Function;
 
 /**
  * Preference with a link and summary about what apps can break through the mode
@@ -64,24 +68,26 @@
     private ZenMode mZenMode;
     private CircularIconsPreference mPreference;
     private final Fragment mHost;
+    private final Function<ApplicationInfo, Drawable> mAppIconRetriever;
 
     ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
             ZenModesBackend backend, ZenHelperBackend helperBackend) {
         this(context, key, host,
                 ApplicationsState.getInstance((Application) context.getApplicationContext()),
-                backend, helperBackend);
+                backend, helperBackend, appInfo -> Utils.getBadgedIcon(context, appInfo));
     }
 
     @VisibleForTesting
     ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
             ApplicationsState applicationsState, ZenModesBackend backend,
-            ZenHelperBackend helperBackend) {
+            ZenHelperBackend helperBackend, Function<ApplicationInfo, Drawable> appIconRetriever) {
         super(context, key, backend);
         mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend);
         mHelperBackend = helperBackend;
         mApplicationsState = applicationsState;
         mUserManager = context.getSystemService(UserManager.class);
         mHost = host;
+        mAppIconRetriever = appIconRetriever;
     }
 
     @Override
@@ -93,10 +99,9 @@
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
         bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        // TODO(b/332937635): Update metrics category
         preference.setIntent(
                 ZenSubSettingLauncher.forModeFragment(mContext, ZenModeAppsFragment.class,
-                        zenMode.getId(), 0).toIntent());
+                        zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
         preference.setEnabled(zenMode.isEnabled());
 
         mZenMode = zenMode;
@@ -105,13 +110,18 @@
         if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
             mPreference.setSummary(R.string.zen_mode_apps_none_apps);
             mPreference.displayIcons(CircularIconSet.EMPTY);
+            if (mAppSession != null) {
+                mAppSession.deactivateSession();
+            }
         } else {
             if (TextUtils.isEmpty(mPreference.getSummary())) {
                 mPreference.setSummary(R.string.zen_mode_apps_calculating);
             }
-            if (mApplicationsState != null && mHost != null) {
+            if (mAppSession == null) {
                 mAppSession = mApplicationsState.newSession(mAppSessionCallbacks,
                         mHost.getLifecycle());
+            } else {
+                mAppSession.activateSession();
             }
             triggerUpdateAppsBypassingDnd();
         }
@@ -133,12 +143,16 @@
     }
 
     private void displayAppsBypassingDnd(List<AppEntry> allApps) {
+        if (mZenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
+            // Can get this callback when resuming, if we had CHANNEL_POLICY_PRIORITY and just
+            // switched to CHANNEL_POLICY_NONE.
+            return;
+        }
+
         ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);
-
         mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
-
         mPreference.displayIcons(new CircularIconSet<>(apps,
-                app -> Utils.getBadgedIcon(mContext, app.info)),
+                app -> mAppIconRetriever.apply(app.info)),
                 APP_ENTRY_EQUIVALENCE);
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
index 522f191..c44661a 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
@@ -106,10 +106,9 @@
         if (mModeId != null) {
             bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, mModeId);
         }
-        // TODO(b/332937635): Update metrics category
         new SubSettingLauncher(mContext)
                 .setDestination(ZenModeSelectBypassingAppsFragment.class.getName())
-                .setSourceMetricsCategory(SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS)
+                .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS)
                 .setArguments(bundle)
                 .launch();
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java
index 54072ac..ac05328 100644
--- a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java
@@ -50,7 +50,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.DND_CALLS;
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
index d885019..efddcf9 100644
--- a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
@@ -18,6 +18,7 @@
 
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 
@@ -41,10 +42,9 @@
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
         bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeCallsFragment.class.getName())
-                .setSourceMetricsCategory(0)
+                .setSourceMetricsCategory(SettingsEnums.DND_PEOPLE)
                 .setArguments(bundle)
                 .toIntent());
         preference.setSummary(mSummaryHelper.getCallsSettingSummary(zenMode));
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java
index 38ac8f3..74ed38f 100644
--- a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java
@@ -54,7 +54,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return SettingsEnums.DND_PEOPLE;
+        return SettingsEnums.ZEN_MODE_DISPLAY_SETTINGS;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
index bba5e34..57dce89 100644
--- a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
@@ -18,6 +18,7 @@
 
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 
@@ -41,10 +42,9 @@
     void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
         bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        // TODO(b/332937635): Update metrics category
         preference.setIntent(
                 ZenSubSettingLauncher.forModeFragment(mContext, ZenModeDisplayFragment.class,
-                        zenMode.getId(), 0).toIntent());
+                        zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
         preference.setEnabled(zenMode.isEnabled());
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java
index a0c2cf1..60f7316 100644
--- a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragment.java
@@ -72,8 +72,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
+        return SettingsEnums.ZEN_MODE_EDIT_NAME_ICON;
     }
 
     @Override
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 0a80977..3777299 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -129,8 +129,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
+        return SettingsEnums.ZEN_PRIORITY_MODE;
     }
 
     @Override
@@ -164,9 +163,8 @@
         @Override
         public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
             if (menuItem.getItemId() == RENAME_MODE) {
-                // TODO: b/332937635 - Update metrics category
                 ZenSubSettingLauncher.forModeFragment(mContext, ZenModeEditNameIconFragment.class,
-                        mZenMode.getId(), 0).launch();
+                        mZenMode.getId(), getMetricsCategory()).launch();
             } else if (menuItem.getItemId() == DELETE_MODE) {
                 new AlertDialog.Builder(mContext)
                         .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation,
diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java
index 8bf574f..709e5da 100644
--- a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java
@@ -46,7 +46,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.DND_MESSAGES;
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
index 4c0b758..50d7958 100644
--- a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
@@ -18,6 +18,7 @@
 
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 
@@ -40,10 +41,9 @@
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
         bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeMessagesFragment.class.getName())
-                .setSourceMetricsCategory(0)
+                .setSourceMetricsCategory(SettingsEnums.DND_PEOPLE)
                 .setArguments(bundle)
                 .toIntent());
 
diff --git a/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java b/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java
index 6086c0c..d7dbaaf 100644
--- a/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeNewCustomFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.settings.notification.modes;
 
+import android.app.settings.SettingsEnums;
+
 import androidx.annotation.Nullable;
 
 import com.android.settings.R;
@@ -50,15 +52,15 @@
         if (created != null) {
             // Open the mode view fragment and close the "add mode" fragment, so exiting the mode
             // view goes back to previous screen (which should be the modes list).
-            ZenSubSettingLauncher.forMode(requireContext(), created.getId()).launch();
+            ZenSubSettingLauncher.forModeFragment(requireContext(), ZenModeFragment.class,
+                    created.getId(), getMetricsCategory()).launch();
             finish();
         }
     }
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return 0;
+        return SettingsEnums.ZEN_MODE_ADD_NEW;
     }
 
     @Override
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java
index 3fdfec6..d1bd493 100644
--- a/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java
@@ -19,6 +19,7 @@
 import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.service.notification.ZenPolicy;
+
 import com.android.settings.R;
 import com.android.settingslib.core.AbstractPreferenceController;
 
@@ -57,7 +58,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
-        return SettingsEnums.DND_PEOPLE;
+        return SettingsEnums.ZEN_CUSTOM_RULE_VIS_EFFECTS;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
index 622c4a2..cd1e8c7 100644
--- a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
@@ -19,6 +19,7 @@
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 
@@ -47,10 +48,9 @@
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
         bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
-        // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeNotifVisFragment.class.getName())
-                .setSourceMetricsCategory(0)
+                .setSourceMetricsCategory(SettingsEnums.ZEN_MODE_DISPLAY_SETTINGS)
                 .setArguments(bundle)
                 .toIntent());
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java
index 1149cd1..28b2e54 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java
@@ -16,14 +16,9 @@
 
 package com.android.settings.notification.modes;
 
-import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS;
-import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS;
-import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA;
-import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
-import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
-
 import android.app.settings.SettingsEnums;
 import android.content.Context;
+
 import com.android.settings.R;
 import com.android.settingslib.core.AbstractPreferenceController;
 
@@ -58,7 +53,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
index 15e0edc..5b26364 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
@@ -23,6 +23,7 @@
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.service.notification.ZenPolicy;
 
@@ -65,10 +66,9 @@
 
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
-        // TODO: b/332937635 - Update metrics category
         preference.setIntent(
                 ZenSubSettingLauncher.forModeFragment(mContext, ZenModeOtherFragment.class,
-                        zenMode.getId(), 0).toIntent());
+                        zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
 
         preference.setEnabled(zenMode.isEnabled());
         preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java
index f541d13..11e4453 100644
--- a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java
@@ -48,7 +48,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.DND_PEOPLE;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
index 4610c35ca..9aad460 100644
--- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
@@ -26,6 +26,7 @@
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
 import static android.service.notification.ZenPolicy.STATE_ALLOW;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.graphics.drawable.Drawable;
@@ -88,10 +89,10 @@
 
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
-        // TODO(b/332937635): Update metrics category
+        // Passes in source ZenModeFragment metric category.
         preference.setIntent(
                 ZenSubSettingLauncher.forModeFragment(mContext, ZenModePeopleFragment.class,
-                        zenMode.getId(), 0).toIntent());
+                        zenMode.getId(), SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
 
         preference.setEnabled(zenMode.isEnabled());
         preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy()));
diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
index ab5e2d9..11b65bd 100644
--- a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
@@ -270,10 +270,9 @@
                 mContext.startActivity(ALL_CONTACTS_INTENT);
             } else if (KEY_ANY_CONVERSATIONS.equals(key)
                     || KEY_IMPORTANT_CONVERSATIONS.equals(key)) {
-                // TODO: b/332937635 - set correct metrics category
                 new SubSettingLauncher(mContext)
                         .setDestination(ConversationListSettings.class.getName())
-                        .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS)
+                        .setSourceMetricsCategory(SettingsEnums.DND_MESSAGES)
                         .launch();
             } else {
                 mContext.startActivity(FALLBACK_INTENT);
diff --git a/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java
index 6202648..d129aad 100644
--- a/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java
+++ b/src/com/android/settings/notification/modes/ZenModeScheduleChooserDialog.java
@@ -20,6 +20,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import android.app.Dialog;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.net.Uri;
 import android.os.Bundle;
@@ -70,8 +71,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - Update metrics category
-        return 0;
+        return SettingsEnums.ZEN_SCHEDULE_CHOOSER_DIALOG;
     }
 
     static void show(DashboardFragment parent, OnScheduleOptionListener optionListener) {
diff --git a/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java
index 8b682b9..1f5438d 100644
--- a/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java
@@ -74,8 +74,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO(b/332937635): Update metrics category
-        return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS;
+        return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP;
     }
 
     /**
diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java
index f0206ef..a266c8b 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java
@@ -46,7 +46,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.NOTIFICATION_ZEN_MODE_EVENT_RULE;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
index 4d58097..9119784 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
@@ -48,7 +48,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
index d8e1b38..3fa5394 100644
--- a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
@@ -62,7 +62,6 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - set correct metrics category (or decide to keep this one?)
         return SettingsEnums.DIALOG_ZEN_TIMEPICKER;
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
index 885c4db..1add488 100644
--- a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
@@ -24,6 +24,7 @@
 
 import android.annotation.SuppressLint;
 import android.app.AlertDialog;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -97,9 +98,9 @@
 
     private void setUpForSystemOwnedTrigger(Preference preference, ZenMode mode) {
         if (mode.getType() == TYPE_SCHEDULE_TIME) {
-            // TODO: b/332937635 - set correct metrics category
             preference.setIntent(ZenSubSettingLauncher.forModeFragment(mContext,
-                    ZenModeSetScheduleFragment.class, mode.getId(), 0).toIntent());
+                    ZenModeSetScheduleFragment.class, mode.getId(),
+                    SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
 
             // [Clock Icon] 9:00 - 17:00 / Sun-Mon
             preference.setIcon(com.android.internal.R.drawable.ic_zen_mode_type_schedule_time);
@@ -115,9 +116,9 @@
                 preference.setSummary(null);
             }
         } else if (mode.getType() == TYPE_SCHEDULE_CALENDAR) {
-            // TODO: b/332937635 - set correct metrics category
             preference.setIntent(ZenSubSettingLauncher.forModeFragment(mContext,
-                    ZenModeSetCalendarFragment.class, mode.getId(), 0).toIntent());
+                    ZenModeSetCalendarFragment.class, mode.getId(),
+                    SettingsEnums.ZEN_PRIORITY_MODE).toIntent());
 
             // [Event Icon] Calendar Events / <Calendar name>
             preference.setIcon(
diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java
index 57d3bf9..e7905a8 100644
--- a/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java
+++ b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java
@@ -20,6 +20,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import android.app.Dialog;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -56,8 +57,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - Update metrics category
-        return 0;
+        return SettingsEnums.ZEN_MODE_NEW_TYPE_CHOOSER_DIALOG;
     }
 
     static void show(DashboardFragment parent,
diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java
index cab0209..2b58f8e 100644
--- a/src/com/android/settings/notification/modes/ZenModesListFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java
@@ -74,8 +74,7 @@
 
     @Override
     public int getMetricsCategory() {
-        // TODO: b/332937635 - add new & set metrics categories correctly
-        return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
+        return SettingsEnums.ZEN_PRIORITY_MODES_LIST;
     }
 
     private void onAvailableModeTypesForAdd(List<ModeType> types) {
@@ -97,10 +96,9 @@
             startActivityForResult(type.creationActivityIntent(), REQUEST_NEW_MODE);
         } else {
             // Custom-manual mode -> "add a mode" screen.
-            // TODO: b/332937635 - set metrics categories correctly
             new SubSettingLauncher(requireContext())
                     .setDestination(ZenModeNewCustomFragment.class.getName())
-                    .setSourceMetricsCategory(0)
+                    .setSourceMetricsCategory(SettingsEnums.ZEN_PRIORITY_MODES_LIST)
                     .launch();
         }
     }
@@ -125,7 +123,9 @@
                 .filter(m -> m.getRule().getPackageName().equals(activityInvoked.getPackageName()))
                 .findFirst();
         createdZenMode.ifPresent(
-                mode -> ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch());
+                mode ->
+                        ZenSubSettingLauncher.forModeFragment(mContext, ZenModeFragment.class,
+                                mode.getId(), getMetricsCategory()).launch());
     }
 
     /**
diff --git a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
index e09d04c..0c96148 100644
--- a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
+++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
@@ -15,6 +15,7 @@
  */
 package com.android.settings.notification.modes;
 
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.widget.TextView;
 
@@ -63,7 +64,8 @@
 
     @Override
     public void onClick() {
-        ZenSubSettingLauncher.forMode(mContext, mZenMode.getId()).launch();
+        ZenSubSettingLauncher.forModeFragment(mContext, ZenModeFragment.class, mZenMode.getId(),
+                SettingsEnums.ZEN_PRIORITY_MODES_LIST).launch();
     }
 
     public void setZenMode(ZenMode zenMode) {
diff --git a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
index 00c21bb..c02a9d9 100644
--- a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
+++ b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
@@ -18,7 +18,6 @@
 
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
-import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
 
@@ -26,12 +25,6 @@
 import com.android.settings.dashboard.DashboardFragment;
 
 class ZenSubSettingLauncher {
-
-    static SubSettingLauncher forMode(Context context, String modeId) {
-        return forModeFragment(context, ZenModeFragment.class, modeId,
-                SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION);
-    }
-
     static SubSettingLauncher forModeFragment(Context context,
             Class<? extends DashboardFragment> fragmentClass, String modeId,
             int sourceMetricsCategory) {
diff --git a/src/com/android/settings/slices/SlicePreferenceController.java b/src/com/android/settings/slices/SlicePreferenceController.java
index 5e8fb26..2e835a0 100644
--- a/src/com/android/settings/slices/SlicePreferenceController.java
+++ b/src/com/android/settings/slices/SlicePreferenceController.java
@@ -20,6 +20,7 @@
 import android.net.Uri;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.Observer;
@@ -61,7 +62,8 @@
         return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
     }
 
-    public void setSliceUri(Uri uri) {
+    /** Sets Slice uri for the preference. */
+    public void setSliceUri(@Nullable Uri uri) {
         mUri = uri;
         mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
             Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
diff --git a/src/com/android/settings/users/UserCapabilities.java b/src/com/android/settings/users/UserCapabilities.java
index 590cb0c..60e92a8 100644
--- a/src/com/android/settings/users/UserCapabilities.java
+++ b/src/com/android/settings/users/UserCapabilities.java
@@ -76,6 +76,9 @@
     public void updateAddUserCapabilities(Context context) {
         final UserManager userManager =
                 (UserManager) context.getSystemService(Context.USER_SERVICE);
+        final UserInfo myUserInfo = userManager.getUserInfo(UserHandle.myUserId());
+        mIsAdmin = myUserInfo.isAdmin();
+
         mEnforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context,
                 UserManager.DISALLOW_ADD_USER, UserHandle.myUserId());
         final boolean hasBaseUserRestriction = RestrictedLockUtilsInternal.hasBaseUserRestriction(
diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java
index 66c278e..8afab96 100644
--- a/src/com/android/settings/users/UserDetailsSettings.java
+++ b/src/com/android/settings/users/UserDetailsSettings.java
@@ -570,7 +570,9 @@
      *   <li>OR multiple admin support is NOT enabled.</li>
      *   <li>OR the <b>current</b> user has DISALLOW_GRANT_ADMIN restriction applied</li>
      *
-     *   <li>OR the <b>target</b> user ('mUserInfo') is a main user OR a guest user.</li>
+     *   <li>OR the <b>target</b> user ('mUserInfo') is a main user</li>
+     *   <li>OR the <b>target</b> user ('mUserInfo') is not of type
+     *   {@link UserManager#USER_TYPE_FULL_SECONDARY}</li>
      *   <li>OR the <b>target</b> user ('mUserInfo') has DISALLOW_GRANT_ADMIN restriction.</li>
      * </ul>
      *
@@ -582,7 +584,7 @@
                 || mUserManager.hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN);
 
         boolean targetUserRestricted = mUserInfo.isMain()
-                || mUserInfo.isGuest()
+                || !(UserManager.USER_TYPE_FULL_SECONDARY.equals(mUserInfo.userType))
                 || mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_GRANT_ADMIN,
                 mUserInfo.getUserHandle());
 
diff --git a/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java
index 5fd11f9..6797061 100644
--- a/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/DaltonizerSaturationSeekbarPreferenceControllerTest.java
@@ -58,8 +58,6 @@
     private ContentResolver mContentResolver;
     private DaltonizerSaturationSeekbarPreferenceController mController;
 
-    private int mOriginalSaturationLevel = -1;
-
     private PreferenceScreen mScreen;
     private LifecycleOwner mLifecycleOwner;
     private Lifecycle mLifecycle;
@@ -73,10 +71,6 @@
     public void setup() {
         Context context = ApplicationProvider.getApplicationContext();
         mContentResolver = context.getContentResolver();
-        mOriginalSaturationLevel = Settings.Secure.getInt(
-                mContentResolver,
-                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL,
-                7);
 
         mPreference = new SeekBarPreference(context);
         mPreference.setKey(ToggleDaltonizerPreferenceFragment.KEY_SATURATION);
@@ -92,10 +86,18 @@
 
     @After
     public void cleanup() {
-        Settings.Secure.putInt(
+        Settings.Secure.putString(
+                mContentResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER,
+                null);
+        Settings.Secure.putString(
+                mContentResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+                null);
+        Settings.Secure.putString(
                 mContentResolver,
                 Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL,
-                mOriginalSaturationLevel);
+                null);
     }
 
     @Test
@@ -113,6 +115,22 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION)
+    public void getAvailabilityStatus_defaultSettings_unavailable() {
+        // By default enabled == false.
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION)
+    public void getAvailabilityStatus_enabledDefaultDisplayMode_available() {
+        setDaltonizerEnabled(1);
+
+        // By default display mode is deuteranomaly.
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_COLOR_CORRECTION_SATURATION)
     public void getAvailabilityStatus_flagEnabledProtanEnabled_available() {
         setDaltonizerMode(/* enabled= */ 1, /* mode= */ 11);
         assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
@@ -306,10 +324,7 @@
         mLifecycle.addObserver(mController);
         mLifecycle.handleLifecycleEvent(ON_RESUME);
 
-        Settings.Secure.putInt(
-                mContentResolver,
-                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
-                1);
+        setDaltonizerEnabled(1);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(mPreference.isEnabled()).isTrue();
@@ -324,10 +339,7 @@
         mLifecycle.addObserver(mController);
         mLifecycle.handleLifecycleEvent(ON_RESUME);
 
-        Settings.Secure.putInt(
-                mContentResolver,
-                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
-                0);
+        setDaltonizerEnabled(0);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(mPreference.isEnabled()).isFalse();
@@ -342,10 +354,7 @@
         mLifecycle.addObserver(mController);
         mLifecycle.handleLifecycleEvent(ON_RESUME);
 
-        Settings.Secure.putInt(
-                mContentResolver,
-                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER,
-                0);
+        setDaltonizerDisplay(0);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(mPreference.isEnabled()).isFalse();
@@ -361,23 +370,28 @@
         mLifecycle.handleLifecycleEvent(ON_STOP);
 
         // enabled.
-        Settings.Secure.putInt(
-                mContentResolver,
-                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
-                1);
+        setDaltonizerEnabled(1);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(mPreference.isEnabled()).isFalse();
     }
 
     private void setDaltonizerMode(int enabled, int mode) {
+        setDaltonizerEnabled(enabled);
+        setDaltonizerDisplay(mode);
+    }
+
+    private void setDaltonizerEnabled(int enabled) {
         Settings.Secure.putInt(
                 mContentResolver,
                 Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
                 enabled);
-        Settings.Secure.putInt(
+    }
+
+    private void setDaltonizerDisplay(int mode) {
+        Settings.Secure.putString(
                 mContentResolver,
                 Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER,
-                mode);
+                Integer.toString(mode));
     }
 }
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
index 50aa771..19d0edd 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
@@ -50,6 +50,7 @@
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
 import com.android.settings.testutils.FakeFeatureFactory;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -101,6 +102,8 @@
     private InputManager mInputManager;
     @Mock
     private CompanionDeviceManager mCompanionDeviceManager;
+    @Mock
+    private DeviceDetailsFragmentFormatter mFormatter;
 
     @Before
     public void setUp() {
@@ -111,7 +114,10 @@
                 .getSystemService(CompanionDeviceManager.class);
         when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
         removeInputDeviceWithMatchingBluetoothAddress();
-        FakeFeatureFactory.setupForTest();
+        FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
+        when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
+                any(), any(), eq(mCachedDevice))).thenReturn(mFormatter);
+        when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null);
 
         mFragment = setupFragment();
         mFragment.onAttach(mContext);
diff --git a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt
new file mode 100644
index 0000000..a83b7c2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.domain.interactor
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
+import android.content.Context
+import android.media.AudioDeviceAttributes
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import androidx.test.core.app.ApplicationProvider
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.media.data.repository.SpatializerRepository
+import com.android.settingslib.media.domain.interactor.SpatializerInteractor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class SpatialAudioInteractorTest {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var audioManager: AudioManager
+    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+    @Mock private lateinit var bluetoothDevice: BluetoothDevice
+    @Mock private lateinit var spatializerRepository: SpatializerRepository
+    @Mock private lateinit var leAudioProfile: LeAudioProfile
+
+    private lateinit var underTest: SpatialAudioInteractor
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        val context = spy(ApplicationProvider.getApplicationContext<Context>())
+        `when`(cachedDevice.device).thenReturn(bluetoothDevice)
+        `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
+        `when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+        underTest =
+            SpatialAudioInteractorImpl(
+                context,
+                audioManager,
+                SpatializerInteractor(spatializerRepository),
+                testScope.backgroundScope,
+                testScope.testScheduler)
+    }
+
+    @Test
+    fun getDeviceSetting_noAudioProfile_returnNull() {
+        testScope.runTest {
+            val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
+
+            assertThat(setting).isNull()
+            verifyNoInteractions(spatializerRepository)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
+
+            val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
+
+            assertThat(setting).isNull()
+            verifyNoInteractions(spatializerRepository)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
+            `when`(
+                    spatializerRepository.isSpatialAudioAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(false)
+
+            val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
+
+            assertThat(setting).isNull()
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
+            `when`(
+                    spatializerRepository.isSpatialAudioAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(
+                    spatializerRepository.isHeadTrackingAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(false)
+            `when`(spatializerRepository.getSpatialAudioCompatibleDevices())
+                .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
+            `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(false)
+
+            val setting =
+                getLatestValue(underTest.getDeviceSetting(cachedDevice))
+                    as DeviceSettingModel.MultiTogglePreference
+
+            assertThat(setting).isNotNull()
+            assertThat(setting.toggles.size).isEqualTo(2)
+            assertThat(setting.state.selectedIndex).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
+            `when`(
+                    spatializerRepository.isSpatialAudioAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(
+                    spatializerRepository.isHeadTrackingAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(spatializerRepository.getSpatialAudioCompatibleDevices())
+                .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
+            `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+
+            val setting =
+                getLatestValue(underTest.getDeviceSetting(cachedDevice))
+                    as DeviceSettingModel.MultiTogglePreference
+
+            assertThat(setting).isNotNull()
+            assertThat(setting.toggles.size).isEqualTo(3)
+            assertThat(setting.state.selectedIndex).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_updateState_enableSpatialAudio() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
+            `when`(
+                    spatializerRepository.isSpatialAudioAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(
+                    spatializerRepository.isHeadTrackingAvailableForDevice(
+                        BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
+            `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(false)
+
+            val setting =
+                getLatestValue(underTest.getDeviceSetting(cachedDevice))
+                    as DeviceSettingModel.MultiTogglePreference
+            setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
+            runCurrent()
+
+            assertThat(setting).isNotNull()
+            verify(spatializerRepository, times(1))
+                .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_updateState_enableHeadTracking() {
+        testScope.runTest {
+            `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
+            `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
+            `when`(
+                spatializerRepository.isSpatialAudioAvailableForDevice(
+                    BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(
+                spatializerRepository.isHeadTrackingAvailableForDevice(
+                    BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(true)
+            `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
+            `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
+                .thenReturn(false)
+
+            val setting =
+                getLatestValue(underTest.getDeviceSetting(cachedDevice))
+                    as DeviceSettingModel.MultiTogglePreference
+            setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
+            runCurrent()
+
+            assertThat(setting).isNotNull()
+            verify(spatializerRepository, times(1))
+                .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
+            verify(spatializerRepository, times(1))
+                .setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true)
+        }
+    }
+
+    private fun getLatestValue(deviceSettingFlow: Flow<DeviceSettingModel?>): DeviceSettingModel? {
+        var latestValue: DeviceSettingModel? = null
+        deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope)
+        testScope.runCurrent()
+        return latestValue
+    }
+
+    private companion object {
+        const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34"
+        val BLE_AUDIO_DEVICE_ATTRIBUTES =
+            AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_OUTPUT,
+                AudioDeviceInfo.TYPE_BLE_HEADSET,
+                BLUETOOTH_ADDRESS,
+            )
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
new file mode 100644
index 0000000..609d767
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.view
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.graphics.Bitmap
+import android.media.AudioManager
+import androidx.fragment.app.FragmentActivity
+import androidx.preference.Preference
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.testutils.FakeFeatureFactory
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.shadows.ShadowLooper.shadowMainLooper
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceDetailsFragmentFormatterTest {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+    @Mock private lateinit var repository: DeviceSettingRepository
+    @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
+
+    private lateinit var fragment: TestFragment
+    private lateinit var underTest: DeviceDetailsFragmentFormatter
+    private lateinit var featureFactory: FakeFeatureFactory
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        featureFactory = FakeFeatureFactory.setupForTest()
+        `when`(
+                featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+                    eq(context), eq(bluetoothAdapter), any()))
+            .thenReturn(repository)
+        `when`(
+            featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
+                eq(context), any(AudioManager::class.java), any()))
+            .thenReturn(spatialAudioInteractor)
+        val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
+        assertThat(fragmentActivity.applicationContext).isNotNull()
+        fragment = TestFragment(context)
+        fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit()
+        shadowMainLooper().idle()
+
+        fragment.preferenceScreen.run {
+            addPreference(Preference(context).apply { key = "bluetooth_device_header" })
+            addPreference(Preference(context).apply { key = "action_buttons" })
+            addPreference(Preference(context).apply { key = "keyboard_settings" })
+        }
+
+        underTest =
+            DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
+    }
+
+    @Test
+    fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+                                "bluetooth_device_header"),
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
+                        ),
+                        listOf(),
+                        "footer"))
+
+            val keys = underTest.getVisiblePreferenceKeysForMainPage()
+
+            assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons")
+        }
+    }
+
+    @Test
+    fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
+
+            val keys = underTest.getVisiblePreferenceKeysForMainPage()
+
+            assertThat(keys).isNull()
+        }
+    }
+
+    @Test
+    fun updateLayout_configIsNull_notChange() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
+
+            underTest.updateLayout()
+
+            assertThat(getDisplayedPreferences().map { it.key })
+                .containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings")
+        }
+    }
+
+    @Test
+    fun updateLayout_itemsNotInConfig_hide() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+                                "bluetooth_device_header"),
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
+                                "keyboard_settings"),
+                        ),
+                        listOf(),
+                        "footer"))
+
+            underTest.updateLayout()
+
+            assertThat(getDisplayedPreferences().map { it.key })
+                .containsExactly("bluetooth_device_header", "keyboard_settings")
+        }
+    }
+
+    @Test
+    fun updateLayout_newItems_displayNewItems() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+                                "bluetooth_device_header"),
+                            DeviceSettingConfigItemModel.AppProvidedItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_ANC),
+                            DeviceSettingConfigItemModel.BuiltinItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
+                                "keyboard_settings"),
+                        ),
+                        listOf(),
+                        "footer"))
+            `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
+                .thenReturn(
+                    flowOf(
+                        DeviceSettingModel.MultiTogglePreference(
+                            cachedDevice,
+                            DeviceSettingId.DEVICE_SETTING_ID_ANC,
+                            "title",
+                            toggles =
+                                listOf(
+                                    ToggleModel(
+                                        "", DeviceSettingIcon.BitmapIcon(
+                                            Bitmap.createBitmap(
+                                                1,
+                                                1,
+                                                Bitmap.Config.ARGB_8888
+                                            )
+                                        )
+                                    )
+                                ),
+                            isActive = true,
+                            state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
+                            isAllowedChangingState = true,
+                            updateState = {})))
+
+            underTest.updateLayout()
+
+            assertThat(getDisplayedPreferences().map { it.key })
+                .containsExactly(
+                    "bluetooth_device_header",
+                    "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
+                    "keyboard_settings")
+        }
+    }
+
+    private fun getDisplayedPreferences(): List<Preference> {
+        val prefs = mutableListOf<Preference>()
+        for (i in 0..<fragment.preferenceScreen.preferenceCount) {
+            prefs.add(fragment.preferenceScreen.getPreference(i))
+        }
+        return prefs
+    }
+
+    class TestFragment(context: Context) : DashboardFragment() {
+        private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
+
+        init {
+            mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
+        }
+
+        public override fun getPreferenceScreenResId(): Int = 0
+
+        override fun getLogTag(): String = "TestLogTag"
+
+        override fun getPreferenceScreen(): PreferenceScreen {
+            return mPreferenceManager.preferenceScreen
+        }
+
+        override fun getMetricsCategory(): Int = 0
+
+        override fun getPreferenceManager(): PreferenceManager {
+            return mPreferenceManager
+        }
+    }
+
+    private companion object {}
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
new file mode 100644
index 0000000..a1fadb8
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.viewmodel
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.testutils.FakeFeatureFactory
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class BluetoothDeviceDetailsViewModelTest {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+
+    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+    @Mock private lateinit var repository: DeviceSettingRepository
+
+    @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
+
+    private lateinit var underTest: BluetoothDeviceDetailsViewModel
+    private lateinit var featureFactory: FakeFeatureFactory
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        featureFactory = FakeFeatureFactory.setupForTest()
+        `when`(
+                featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+                    eq(context), eq(bluetoothAdapter), any()))
+            .thenReturn(repository)
+
+        underTest =
+            BluetoothDeviceDetailsViewModel(repository, spatialAudioInteractor, cachedDevice)
+    }
+
+    @Test
+    fun getItems_returnConfigMainItems() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
+
+            val keys = underTest.getItems()
+
+            assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_returnRepositoryResponse() {
+        testScope.runTest {
+            val remoteSettingId1 = 10001
+            val pref = buildMultiTogglePreference(remoteSettingId1)
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            BUILTIN_SETTING_ITEM_1,
+                            buildRemoteSettingItem(remoteSettingId1),
+                        ),
+                        listOf(),
+                        "footer"))
+            `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
+                .thenReturn(flowOf(pref))
+
+            var deviceSetting: DeviceSettingModel? = null
+            underTest
+                .getDeviceSetting(cachedDevice, remoteSettingId1)
+                .onEach { deviceSetting = it }
+                .launchIn(testScope.backgroundScope)
+            runCurrent()
+
+            assertThat(deviceSetting).isSameInstanceAs(pref)
+            verify(repository, times(1)).getDeviceSetting(cachedDevice, remoteSettingId1)
+        }
+    }
+
+    @Test
+    fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() {
+        testScope.runTest {
+            val pref =
+                buildMultiTogglePreference(
+                    DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            BUILTIN_SETTING_ITEM_1,
+                            buildRemoteSettingItem(
+                                DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE),
+                        ),
+                        listOf(),
+                        "footer"))
+            `when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref))
+
+            var deviceSetting: DeviceSettingModel? = null
+            underTest
+                .getDeviceSetting(
+                    cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
+                .onEach { deviceSetting = it }
+                .launchIn(testScope.backgroundScope)
+            runCurrent()
+
+            assertThat(deviceSetting).isSameInstanceAs(pref)
+            verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice)
+        }
+    }
+
+    @Test
+    fun getLayout_builtinDeviceSettings() {
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
+
+            val layout = underTest.getLayout()!!
+
+            assertThat(getLatestLayout(layout))
+                .isEqualTo(
+                    listOf(
+                        listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
+                        listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS)))
+        }
+    }
+
+    @Test
+    fun getLayout_remoteDeviceSettings() {
+        val remoteSettingId1 = 10001
+        val remoteSettingId2 = 10002
+        val remoteSettingId3 = 10003
+        testScope.runTest {
+            `when`(repository.getDeviceSettingsConfig(cachedDevice))
+                .thenReturn(
+                    DeviceSettingConfigModel(
+                        listOf(
+                            BUILTIN_SETTING_ITEM_1,
+                            buildRemoteSettingItem(remoteSettingId1),
+                            buildRemoteSettingItem(remoteSettingId2),
+                            buildRemoteSettingItem(remoteSettingId3),
+                        ),
+                        listOf(),
+                        "footer"))
+            `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
+                .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1)))
+            `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2))
+                .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2)))
+            `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
+                .thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
+
+            val layout = underTest.getLayout()!!
+
+            assertThat(getLatestLayout(layout))
+                .isEqualTo(
+                    listOf(
+                        listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
+                        listOf(remoteSettingId1, remoteSettingId2),
+                        listOf(remoteSettingId3),
+                    ))
+        }
+    }
+
+    private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
+        var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
+        for (i in layout.rows.indices) {
+            layout.rows[i]
+                .settingIds
+                .onEach { latestLayout[i] = it }
+                .launchIn(testScope.backgroundScope)
+        }
+
+        testScope.runCurrent()
+        return latestLayout.filter { !it.isEmpty() }.toList()
+    }
+
+    private fun buildMultiTogglePreference(settingId: Int) =
+        DeviceSettingModel.MultiTogglePreference(
+            cachedDevice,
+            settingId,
+            "title",
+            toggles =
+                listOf(
+                    ToggleModel(
+                        "toggle1",
+                        DeviceSettingIcon.BitmapIcon(
+                            Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
+            isActive = true,
+            state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
+            isAllowedChangingState = true,
+            updateState = {})
+
+    private fun buildActionSwitchPreference(settingId: Int) =
+        DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
+
+    private fun buildRemoteSettingItem(settingId: Int) =
+        DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+
+    private companion object {
+        val BUILTIN_SETTING_ITEM_1 =
+            DeviceSettingConfigItemModel.BuiltinItem(
+                DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
+        val BUILDIN_SETTING_ITEM_2 =
+            DeviceSettingConfigItemModel.BuiltinItem(
+                DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
index 0161178..59021a7 100644
--- a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
@@ -872,6 +872,17 @@
         verify(mWifiEntry, never()).getKey();
     }
 
+    @Test
+    public void launchNetworkDetailsFragment_wifiEntryIsNotSaved_ignoreWifiEntry() {
+        when(mWifiEntry.isSaved()).thenReturn(false);
+        LongPressWifiEntryPreference preference =
+                mNetworkProviderSettings.createLongPressWifiEntryPreference(mWifiEntry);
+
+        mNetworkProviderSettings.launchNetworkDetailsFragment(preference);
+
+        verify(mWifiEntry, never()).getKey();
+    }
+
     @Implements(PreferenceFragmentCompat.class)
     public static class ShadowPreferenceFragmentCompat {
 
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
index 9263ffd..29e9cf9 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
@@ -34,10 +34,12 @@
 import static org.robolectric.Shadows.shadowOf;
 
 import android.app.Flags;
+import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.UserInfo;
+import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -102,11 +104,12 @@
         mContext = RuntimeEnvironment.application;
         CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService();
         mPreference = new TestableCircularIconsPreference(mContext);
-
         when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
+
         mController = new ZenModeAppsLinkPreferenceController(
                 mContext, "controller_key", mock(Fragment.class), mApplicationsState,
-                mZenModesBackend, mHelperBackend);
+                mZenModesBackend, mHelperBackend,
+                /* appIconRetriever= */ appInfo -> new ColorDrawable());
 
         // Ensure the preference view is bound & measured (needed to add child ImageViews).
         View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(),
@@ -163,7 +166,7 @@
         assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
                 .isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
         assertThat(launcherIntent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
-                -1)).isEqualTo(0);
+                -1)).isEqualTo(SettingsEnums.ZEN_PRIORITY_MODE);
 
         Bundle bundle = launcherIntent.getBundleExtra(
                 SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
@@ -296,6 +299,89 @@
     }
 
     @Test
+    public void updateState_noneToPriority_loadsBypassingAppsAndListensForChanges() {
+        ZenMode zenModeWithNone = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
+                .build();
+        ZenMode zenModeWithPriority = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
+                .build();
+        ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
+        appEntries.add(createAppEntry("test", mContext.getUserId()));
+        when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false))
+                .thenReturn(List.of("test"));
+
+        mController.updateState(mPreference, zenModeWithNone);
+
+        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
+        verifyNoMoreInteractions(mApplicationsState);
+        verifyNoMoreInteractions(mSession);
+
+        mController.updateState(mPreference, zenModeWithPriority);
+
+        verify(mApplicationsState).newSession(any(), any());
+        verify(mSession).rebuild(any(), any(), anyBoolean());
+        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
+        assertThat(mPreference.getLoadedIcons().icons()).hasSize(1);
+    }
+
+    @Test
+    public void updateState_priorityToNone_clearsBypassingAppsAndStopsListening() {
+        ZenMode zenModeWithNone = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
+                .build();
+        ZenMode zenModeWithPriority = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
+                .build();
+        ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
+        appEntries.add(createAppEntry("test", mContext.getUserId()));
+        when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(), false))
+                .thenReturn(List.of("test"));
+
+        mController.updateState(mPreference, zenModeWithPriority);
+
+        verify(mApplicationsState).newSession(any(), any());
+        verify(mSession).rebuild(any(), any(), anyBoolean());
+        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
+        assertThat(mPreference.getLoadedIcons().icons()).hasSize(1);
+
+        mController.updateState(mPreference, zenModeWithNone);
+
+        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
+        verify(mSession).deactivateSession();
+        verifyNoMoreInteractions(mSession);
+        verifyNoMoreInteractions(mApplicationsState);
+
+        // An errant callback (triggered by onResume and received asynchronously after
+        // updateState()) is ignored.
+        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
+
+        assertThat(mPreference.getLoadedIcons().icons()).hasSize(0);
+    }
+
+    @Test
+    public void updateState_priorityToNoneToPriority_restartsListening() {
+        ZenMode zenModeWithNone = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(false).build())
+                .build();
+        ZenMode zenModeWithPriority = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowPriorityChannels(true).build())
+                .build();
+
+        mController.updateState(mPreference, zenModeWithPriority);
+        verify(mApplicationsState).newSession(any(), any());
+        verify(mSession).rebuild(any(), any(), anyBoolean());
+
+        mController.updateState(mPreference, zenModeWithNone);
+        verifyNoMoreInteractions(mApplicationsState);
+        verify(mSession).deactivateSession();
+
+        mController.updateState(mPreference, zenModeWithPriority);
+        verifyNoMoreInteractions(mApplicationsState);
+        verify(mSession).activateSession();
+    }
+
+    @Test
     public void testNoCrashIfAppsReadyBeforeRuleAvailable() {
         mController.mAppSessionCallbacks.onLoadEntriesCompleted();
     }
diff --git a/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java b/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java
index a47703c..bec49e1 100644
--- a/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java
+++ b/tests/robotests/src/com/android/settings/users/UserCapabilitiesTest.java
@@ -81,6 +81,17 @@
     }
 
     @Test
+    public void changeAdminStatus_updateUserCapabilities_mIsAdminGetsUpdated() {
+        mUserManager.setIsAdminUser(false);
+        UserCapabilities userCapabilities = UserCapabilities.create(mContext);
+        assertThat(userCapabilities.isAdmin()).isFalse();
+
+        mUserManager.setIsAdminUser(true);
+        userCapabilities.updateAddUserCapabilities(mContext);
+        assertThat(userCapabilities.mIsAdmin).isTrue();
+    }
+
+    @Test
     public void userSwitchEnabled_off() {
         mUserManager.setUserSwitcherEnabled(false);
 
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt
index 4137de4..0cbfe02 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/NetworkSelectRepositoryTest.kt
@@ -17,20 +17,21 @@
 package com.android.settings.network.telephony
 
 import android.content.Context
-import android.telephony.AccessNetworkConstants
-import android.telephony.NetworkRegistrationInfo
-import android.telephony.ServiceState
-import android.telephony.TelephonyManager
+import android.os.PersistableBundle
+import android.telephony.*
+import android.telephony.satellite.SatelliteManager
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settings.network.telephony.scan.NetworkScanRepositoryTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.stub
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 class NetworkSelectRepositoryTest {
@@ -49,8 +50,16 @@
         on { serviceState } doReturn mockServiceState
     }
 
+    private val mockSatelliteManager = mock<SatelliteManager> {
+        on { getSatellitePlmnsForCarrier(anyInt()) } doReturn SatellitePlmns
+    }
+
+    private var mockCarrierConfigManager = mock<CarrierConfigManager>()
+
     private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
         on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager
+        on { getSystemService(SatelliteManager::class.java) } doReturn mockSatelliteManager
+        on { getSystemService(CarrierConfigManager::class.java) } doReturn mockCarrierConfigManager
     }
 
     private val repository = NetworkSelectRepository(context, SUB_ID)
@@ -105,6 +114,14 @@
             on { forbiddenPlmns } doReturn arrayOf(FORBIDDEN_PLMN)
         }
 
+        val config = PersistableBundle()
+        config.putBoolean(
+            CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL,
+            false)
+        whenever(mockCarrierConfigManager.getConfigForSubId(
+            SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL))
+            .thenReturn(config)
+
         val info = repository.getNetworkRegistrationInfo()
 
         assertThat(info).isEqualTo(
@@ -115,9 +132,76 @@
         )
     }
 
+    @Test
+    fun getNetworkRegistrationInfo_filterSatellitePlmn() {
+
+        val info1 = createTestNetworkRegistrationInfo("310", "260")
+        val info2 = createTestNetworkRegistrationInfo("310", "261")
+        val satelliteInfo = createTestNetworkRegistrationInfo(satelliteMcc, satelliteMnc)
+        val registrationInfos = listOf(info1, info2, satelliteInfo)
+        val filteredRegistrationInfos = listOf(info1, info2)
+
+        mockServiceState.stub {
+            on {
+                getNetworkRegistrationInfoListForTransportType(
+                    AccessNetworkConstants.TRANSPORT_TYPE_WWAN
+                )
+            } doReturn registrationInfos
+        }
+        mockTelephonyManager.stub {
+            on { forbiddenPlmns } doReturn arrayOf(FORBIDDEN_PLMN)
+        }
+
+        // disable satellite plmn filter
+        var config = PersistableBundle()
+        config.putBoolean(
+            CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL,
+            false)
+        whenever(mockCarrierConfigManager.getConfigForSubId(
+            SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL))
+            .thenReturn(config)
+
+        var infoList = repository.getNetworkRegistrationInfo()
+
+        assertThat(infoList).isEqualTo(
+            NetworkSelectRepository.NetworkRegistrationAndForbiddenInfo(
+                networkList = registrationInfos,
+                forbiddenPlmns = listOf(FORBIDDEN_PLMN),
+            )
+        )
+
+        // enable satellite plmn filter
+        config = PersistableBundle()
+        config.putBoolean(
+            CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL,
+            true)
+        whenever(mockCarrierConfigManager.getConfigForSubId(
+            SUB_ID, CarrierConfigManager.KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL))
+            .thenReturn(config)
+
+        infoList = repository.getNetworkRegistrationInfo()
+
+        assertThat(infoList).isEqualTo(
+            NetworkSelectRepository.NetworkRegistrationAndForbiddenInfo(
+                networkList = filteredRegistrationInfos,
+                forbiddenPlmns = listOf(FORBIDDEN_PLMN),
+            )
+        )
+    }
+
     private companion object {
         const val SUB_ID = 1
         val NetworkRegistrationInfos = listOf(NetworkRegistrationInfo.Builder().build())
         const val FORBIDDEN_PLMN = "Forbidden PLMN"
+        const val satelliteMcc = "310"
+        const val satelliteMnc = "810"
+        val SatellitePlmns = listOf(satelliteMcc + satelliteMnc)
+
+        fun createTestNetworkRegistrationInfo(mcc: String, mnc: String): NetworkRegistrationInfo {
+            val cellInfo = CellIdentityLte(0, 0, 0, 0, IntArray(2) { 0 },
+                0, mcc, mnc, "", "", emptyList(), null)
+
+            return NetworkRegistrationInfo.Builder().setCellIdentity(cellInfo).build()
+        }
     }
 }