[Ambient Volume] Migrate to use AmbientVolumeUiController in SettingsLib

Moove the common ui logic code into settingslib for using in both
settings and systemui.

Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control
Bug: 357878944
Test: atest AmbientVolumePreferenceTest
Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest
Change-Id: I97d5ac2d1862fed7249af8b35f04fa36fc47d16d
diff --git a/src/com/android/settings/bluetooth/AmbientVolumePreference.java b/src/com/android/settings/bluetooth/AmbientVolumePreference.java
index e916c04..8196edf 100644
--- a/src/com/android/settings/bluetooth/AmbientVolumePreference.java
+++ b/src/com/android/settings/bluetooth/AmbientVolumePreference.java
@@ -21,25 +21,29 @@
 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
 import static android.view.View.VISIBLE;
 
+import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER;
 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
 import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
 
+import android.bluetooth.BluetoothDevice;
 import android.content.Context;
-import android.util.ArrayMap;
 import android.view.View;
 import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceViewHolder;
 
 import com.android.settings.R;
 import com.android.settings.widget.SeekBarPreference;
+import com.android.settingslib.bluetooth.AmbientVolumeUi;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
 import com.google.common.primitives.Ints;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -49,27 +53,13 @@
  * separated control for devices in the same set. Toggle the expand icon will make the UI switch
  * between unified and separated control.
  */
-public class AmbientVolumePreference extends PreferenceGroup {
+public class AmbientVolumePreference extends PreferenceGroup implements AmbientVolumeUi {
 
-    /** Interface definition for a callback to be invoked when the icon is clicked. */
-    public interface OnIconClickListener {
-        /** Called when the expand icon is clicked. */
-        void onExpandIconClick();
-
-        /** Called when the ambient volume icon is clicked. */
-        void onAmbientVolumeIconClick();
-    };
-
-    static final float ROTATION_COLLAPSED = 0f;
-    static final float ROTATION_EXPANDED = 180f;
-    static final int AMBIENT_VOLUME_LEVEL_MIN = 0;
-    static final int AMBIENT_VOLUME_LEVEL_MAX = 24;
-    static final int AMBIENT_VOLUME_LEVEL_DEFAULT = 24;
-    static final int SIDE_UNIFIED = 999;
-    static final List<Integer> VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT);
+    private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0;
+    private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1;
 
     @Nullable
-    private OnIconClickListener mListener;
+    private AmbientVolumeUiListener mListener;
     @Nullable
     private View mExpandIcon;
     @Nullable
@@ -78,27 +68,21 @@
     private boolean mExpanded = false;
     private boolean mMutable = false;
     private boolean mMuted = false;
-    private Map<Integer, SeekBarPreference> mSideToSliderMap = new ArrayMap<>();
-
-    /**
-     * Ambient volume level for hearing device ambient control icon
-     * <p>
-     * This icon visually represents the current ambient gain setting.
-     * It displays separate levels for the left and right sides, each with 5 levels ranging from 0
-     * to 4.
-     * <p>
-     * To represent the combined left/right levels with a single value, the following calculation
-     * is used:
-     *      finalLevel = (leftLevel * 5) + rightLevel
-     * For example:
-     * <ul>
-     *    <li>If left level is 2 and right level is 3, the final level will be 13 (2 * 5 + 3)</li>
-     *    <li>If both left and right levels are 0, the final level will be 0</li>
-     *    <li>If both left and right levels are 4, the final level will be 24</li>
-     * </ul>
-     */
+    private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create();
     private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT;
 
+    private final OnPreferenceChangeListener mPreferenceChangeListener =
+            (slider, v) -> {
+                if (slider instanceof SeekBarPreference && v instanceof final Integer value) {
+                    final Integer side = mSideToSliderMap.inverse().get(slider);
+                    if (mListener != null && side != null) {
+                        mListener.onSliderValueChange(side, value);
+                    }
+                    return true;
+                }
+                return false;
+            };
+
     public AmbientVolumePreference(@NonNull Context context) {
         super(context, null);
         setLayoutResource(R.layout.preference_ambient_volume);
@@ -138,7 +122,8 @@
         updateExpandIcon();
     }
 
-    void setExpandable(boolean expandable) {
+    @Override
+    public void setExpandable(boolean expandable) {
         mExpandable = expandable;
         if (!mExpandable) {
             setExpanded(false);
@@ -146,11 +131,13 @@
         updateExpandIcon();
     }
 
-    boolean isExpandable() {
+    @Override
+    public boolean isExpandable() {
         return mExpandable;
     }
 
-    void setExpanded(boolean expanded) {
+    @Override
+    public void setExpanded(boolean expanded) {
         if (!mExpandable && expanded) {
             return;
         }
@@ -159,11 +146,13 @@
         updateLayout();
     }
 
-    boolean isExpanded() {
+    @Override
+    public boolean isExpanded() {
         return mExpanded;
     }
 
-    void setMutable(boolean mutable) {
+    @Override
+    public void setMutable(boolean mutable) {
         mMutable = mutable;
         if (!mMutable) {
             mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT;
@@ -172,11 +161,13 @@
         updateVolumeIcon();
     }
 
-    boolean isMutable() {
+    @Override
+    public boolean isMutable() {
         return mMutable;
     }
 
-    void setMuted(boolean muted) {
+    @Override
+    public void setMuted(boolean muted) {
         if (!mMutable && muted) {
             return;
         }
@@ -189,25 +180,35 @@
         updateVolumeIcon();
     }
 
-    boolean isMuted() {
+    @Override
+    public boolean isMuted() {
         return mMuted;
     }
 
-    void setOnIconClickListener(@Nullable OnIconClickListener listener) {
+    @Override
+    public void setListener(@Nullable AmbientVolumeUiListener listener) {
         mListener = listener;
     }
 
-    void setSliders(Map<Integer, SeekBarPreference> sideToSliderMap) {
-        mSideToSliderMap = sideToSliderMap;
-        for (SeekBarPreference preference : sideToSliderMap.values()) {
-            if (findPreference(preference.getKey()) == null) {
-                addPreference(preference);
+    @Override
+    public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) {
+        sideToDeviceMap.forEach((side, device) ->
+                createSlider(side, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + side));
+        createSlider(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED);
+
+        if (!mSideToSliderMap.isEmpty()) {
+            for (int side : VALID_SIDES) {
+                final SeekBarPreference slider = mSideToSliderMap.get(side);
+                if (slider != null && findPreference(slider.getKey()) == null) {
+                    addPreference(slider);
+                }
             }
         }
         updateLayout();
     }
 
-    void setSliderEnabled(int side, boolean enabled) {
+    @Override
+    public void setSliderEnabled(int side, boolean enabled) {
         SeekBarPreference slider = mSideToSliderMap.get(side);
         if (slider != null && slider.isEnabled() != enabled) {
             slider.setEnabled(enabled);
@@ -215,7 +216,8 @@
         }
     }
 
-    void setSliderValue(int side, int value) {
+    @Override
+    public void setSliderValue(int side, int value) {
         SeekBarPreference slider = mSideToSliderMap.get(side);
         if (slider != null && slider.getProgress() != value) {
             slider.setProgress(value);
@@ -223,7 +225,8 @@
         }
     }
 
-    void setSliderRange(int side, int min, int max) {
+    @Override
+    public void setSliderRange(int side, int min, int max) {
         SeekBarPreference slider = mSideToSliderMap.get(side);
         if (slider != null) {
             slider.setMin(min);
@@ -231,7 +234,8 @@
         }
     }
 
-    void updateLayout() {
+    @Override
+    public void updateLayout() {
         mSideToSliderMap.forEach((side, slider) -> {
             if (side == SIDE_UNIFIED) {
                 slider.setVisible(!mExpanded);
@@ -279,8 +283,7 @@
         mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE);
         mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED);
         if (mExpandable) {
-            final int stringRes = mExpanded
-                    ? R.string.bluetooth_ambient_volume_control_collapse
+            final int stringRes = mExpanded ? R.string.bluetooth_ambient_volume_control_collapse
                     : R.string.bluetooth_ambient_volume_control_expand;
             mExpandIcon.setContentDescription(getContext().getString(stringRes));
         } else {
@@ -294,8 +297,7 @@
         }
         mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel);
         if (mMutable) {
-            final int stringRes = mMuted
-                    ? R.string.bluetooth_ambient_volume_unmute
+            final int stringRes = mMuted ? R.string.bluetooth_ambient_volume_unmute
                     : R.string.bluetooth_ambient_volume_mute;
             mVolumeIcon.setContentDescription(getContext().getString(stringRes));
             mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -304,4 +306,27 @@
             mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
         }
     }
+
+    private void createSlider(int side, int order) {
+        if (mSideToSliderMap.containsKey(side)) {
+            return;
+        }
+        SeekBarPreference slider = new SeekBarPreference(getContext());
+        slider.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side);
+        slider.setOrder(order);
+        slider.setOnPreferenceChangeListener(mPreferenceChangeListener);
+        if (side == SIDE_LEFT) {
+            slider.setTitle(
+                    getContext().getString(R.string.bluetooth_ambient_volume_control_left));
+        } else if (side == SIDE_RIGHT) {
+            slider.setTitle(
+                    getContext().getString(R.string.bluetooth_ambient_volume_control_right));
+        }
+        mSideToSliderMap.put(side, slider);
+    }
+
+    @VisibleForTesting
+    Map<Integer, SeekBarPreference> getSliders() {
+        return mSideToSliderMap;
+    }
 }
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
index f237ffe..4b0b5d4 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
@@ -16,41 +16,20 @@
 
 package com.android.settings.bluetooth;
 
-import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
-import static android.bluetooth.AudioInputControl.MUTE_MUTED;
-import static android.bluetooth.BluetoothDevice.BOND_BONDED;
-
-import static com.android.settings.bluetooth.AmbientVolumePreference.SIDE_UNIFIED;
-import static com.android.settings.bluetooth.AmbientVolumePreference.VALID_SIDES;
 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP;
 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_AMBIENT_VOLUME;
-import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_INVALID;
-import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
-import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
-import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
 
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
 import android.content.Context;
-import android.util.ArraySet;
-import android.util.Log;
-import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.PreferenceScreen;
 
-import com.android.settings.R;
-import com.android.settings.widget.SeekBarPreference;
-import com.android.settingslib.bluetooth.AmbientVolumeController;
-import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.AmbientVolumeUiController;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager;
-import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.VolumeControlProfile;
 import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -58,39 +37,21 @@
 import com.android.settingslib.core.lifecycle.events.OnStop;
 import com.android.settingslib.utils.ThreadUtils;
 
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-
-import java.util.Map;
-import java.util.Set;
-
-/** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */
-public class BluetoothDetailsAmbientVolumePreferenceController extends
-        BluetoothDetailsController implements Preference.OnPreferenceChangeListener,
-        HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop,
-        AmbientVolumeController.AmbientVolumeControlCallback, BluetoothCallback {
+/** A {@link BluetoothDetailsController} that manages ambient volume preference. */
+public class BluetoothDetailsAmbientVolumePreferenceController extends BluetoothDetailsController
+        implements OnStart, OnStop {
 
     private static final boolean DEBUG = true;
     private static final String TAG = "AmbientPrefController";
 
     static final String KEY_AMBIENT_VOLUME = "ambient_volume";
     static final String KEY_AMBIENT_VOLUME_SLIDER = "ambient_volume_slider";
-    private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0;
-    private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1;
 
     private final LocalBluetoothManager mBluetoothManager;
-    private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>();
-    private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create();
-    private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create();
-    private final HearingDeviceLocalDataManager mLocalDataManager;
-    private final AmbientVolumeController mVolumeController;
-
-    @Nullable
-    private PreferenceCategory mDeviceControls;
     @Nullable
     private AmbientVolumePreference mPreference;
     @Nullable
-    private Toast mToast;
+    private AmbientVolumeUiController mAmbientUiController;
 
     public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
             @NonNull LocalBluetoothManager manager,
@@ -99,45 +60,42 @@
             @NonNull Lifecycle lifecycle) {
         super(context, fragment, device, lifecycle);
         mBluetoothManager = manager;
-        mLocalDataManager = new HearingDeviceLocalDataManager(context);
-        mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
-                ThreadUtils.getBackgroundExecutor());
-        mVolumeController = new AmbientVolumeController(manager.getProfileManager(), this);
     }
 
     @VisibleForTesting
-    BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
+    public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
             @NonNull LocalBluetoothManager manager,
             @NonNull PreferenceFragmentCompat fragment,
             @NonNull CachedBluetoothDevice device,
             @NonNull Lifecycle lifecycle,
-            @NonNull HearingDeviceLocalDataManager localSettings,
-            @NonNull AmbientVolumeController volumeController) {
+            @NonNull AmbientVolumeUiController uiController) {
         super(context, fragment, device, lifecycle);
         mBluetoothManager = manager;
-        mLocalDataManager = localSettings;
-        mVolumeController = volumeController;
+        mAmbientUiController = uiController;
     }
 
     @Override
     protected void init(PreferenceScreen screen) {
-        mDeviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP);
-        if (mDeviceControls == null) {
+        PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP);
+        if (deviceControls == null) {
             return;
         }
-        loadDevices();
+        mPreference = new AmbientVolumePreference(deviceControls.getContext());
+        mPreference.setKey(KEY_AMBIENT_VOLUME);
+        mPreference.setOrder(ORDER_AMBIENT_VOLUME);
+        deviceControls.addPreference(mPreference);
+
+        mAmbientUiController = new AmbientVolumeUiController(mContext, mBluetoothManager,
+                mPreference);
+        mAmbientUiController.loadDevice(mCachedDevice);
     }
 
     @Override
     public void onStart() {
         ThreadUtils.postOnBackgroundThread(() -> {
-            mBluetoothManager.getEventManager().registerCallback(this);
-            mLocalDataManager.start();
-            mCachedDevices.forEach(device -> {
-                device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
-                mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
-                        device.getDevice());
-            });
+            if (mAmbientUiController != null) {
+                mAmbientUiController.start();
+            }
         });
     }
 
@@ -153,12 +111,9 @@
     @Override
     public void onStop() {
         ThreadUtils.postOnBackgroundThread(() -> {
-            mBluetoothManager.getEventManager().unregisterCallback(this);
-            mLocalDataManager.stop();
-            mCachedDevices.forEach(device -> {
-                device.unregisterCallback(this);
-                mVolumeController.unregisterCallback(device.getDevice());
-            });
+            if (mAmbientUiController != null) {
+                mAmbientUiController.stop();
+            }
         });
     }
 
@@ -167,16 +122,8 @@
         if (!isAvailable()) {
             return;
         }
-        boolean shouldShowAmbientControl = isAmbientControlAvailable();
-        if (shouldShowAmbientControl) {
-            if (mPreference != null) {
-                mPreference.setVisible(true);
-            }
-            loadRemoteDataToUi();
-        } else {
-            if (mPreference != null) {
-                mPreference.setVisible(false);
-            }
+        if (mAmbientUiController != null) {
+            mAmbientUiController.refresh();
         }
     }
 
@@ -191,424 +138,4 @@
     public String getPreferenceKey() {
         return KEY_AMBIENT_VOLUME;
     }
-
-    @Override
-    public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) {
-        if (preference instanceof SeekBarPreference && newValue instanceof final Integer value) {
-            final int side = mSideToSliderMap.inverse().getOrDefault(preference, SIDE_INVALID);
-            if (DEBUG) {
-                Log.d(TAG, "onPreferenceChange: side=" + side + ", value=" + value);
-            }
-            setVolumeIfValid(side, value);
-
-            Runnable setAmbientRunnable = () -> {
-                if (side == SIDE_UNIFIED) {
-                    mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
-                } else {
-                    final BluetoothDevice device = mSideToDeviceMap.get(side);
-                    mVolumeController.setAmbient(device, value);
-                }
-            };
-
-            if (isControlMuted()) {
-                // User drag on the volume slider when muted. Unmute the devices first.
-                if (mPreference != null) {
-                    mPreference.setMuted(false);
-                }
-                for (BluetoothDevice device : mSideToDeviceMap.values()) {
-                    mVolumeController.setMuted(device, false);
-                }
-                // Restore the value before muted
-                loadLocalDataToUi();
-                // Delay set ambient on remote device since the immediately sequential command
-                // might get failed sometimes
-                mContext.getMainThreadHandler().postDelayed(setAmbientRunnable, 1000L);
-            } else {
-                setAmbientRunnable.run();
-            }
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
-            int state, int bluetoothProfile) {
-        if (bluetoothProfile == BluetoothProfile.VOLUME_CONTROL
-                && state == BluetoothProfile.STATE_CONNECTED
-                && mCachedDevices.contains(cachedDevice)) {
-            // After VCP connected, AICS may not ready yet and still return invalid value, delay
-            // a while to wait AICS ready as a workaround
-            mContext.getMainThreadHandler().postDelayed(this::refresh, 1000L);
-        }
-    }
-
-    @Override
-    public void onDeviceAttributesChanged() {
-        mCachedDevices.forEach(device -> {
-            device.unregisterCallback(this);
-            mVolumeController.unregisterCallback(device.getDevice());
-        });
-        mContext.getMainExecutor().execute(() -> {
-            loadDevices();
-            if (!mCachedDevices.isEmpty()) {
-                refresh();
-            }
-            ThreadUtils.postOnBackgroundThread(() ->
-                    mCachedDevices.forEach(device -> {
-                        device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
-                        mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
-                                device.getDevice());
-                    })
-            );
-        });
-    }
-
-    @Override
-    public void onDeviceLocalDataChange(@NonNull String address, @Nullable Data data) {
-        if (data == null) {
-            // The local data is removed because the device is unpaired, do nothing
-            return;
-        }
-        for (BluetoothDevice device : mSideToDeviceMap.values()) {
-            if (device.getAnonymizedAddress().equals(address)) {
-                mContext.getMainExecutor().execute(() -> loadLocalDataToUi(device));
-                return;
-            }
-        }
-    }
-
-    @Override
-    public void onVolumeControlServiceConnected() {
-        mCachedDevices.forEach(
-                device -> mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
-                        device.getDevice()));
-    }
-
-    @Override
-    public void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) {
-        if (DEBUG) {
-            Log.d(TAG, "onAmbientChanged, value:" + gainSettings + ", device:" + device);
-        }
-        Data data = mLocalDataManager.get(device);
-        boolean isInitiatedFromUi = (isControlExpanded() && data.ambient() == gainSettings)
-                || (!isControlExpanded() && data.groupAmbient() == gainSettings);
-        if (isInitiatedFromUi) {
-            // The change is initiated from UI, no need to update UI
-            return;
-        }
-
-        // We have to check if we need to expand the controls by getting all remote
-        // device's ambient value, delay for a while to wait all remote devices update
-        // to the latest value to avoid unnecessary expand action.
-        mContext.getMainThreadHandler().postDelayed(this::refresh, 1200L);
-    }
-
-    @Override
-    public void onMuteChanged(@NonNull BluetoothDevice device, int mute) {
-        if (DEBUG) {
-            Log.d(TAG, "onMuteChanged, mute:" + mute + ", device:" + device);
-        }
-        boolean isInitiatedFromUi = (isControlMuted() && mute == MUTE_MUTED)
-                || (!isControlMuted() && mute == MUTE_NOT_MUTED);
-        if (isInitiatedFromUi) {
-            // The change is initiated from UI, no need to update UI
-            return;
-        }
-
-        // We have to check if we need to mute the devices by getting all remote
-        // device's mute state, delay for a while to wait all remote devices update
-        // to the latest value.
-        mContext.getMainThreadHandler().postDelayed(this::refresh, 1200L);
-    }
-
-    @Override
-    public void onCommandFailed(@NonNull BluetoothDevice device) {
-        Log.w(TAG, "onCommandFailed, device:" + device);
-        mContext.getMainExecutor().execute(() -> {
-            showErrorToast();
-            refresh();
-        });
-    }
-
-    private void loadDevices() {
-        mSideToDeviceMap.clear();
-        mCachedDevices.clear();
-        if (VALID_SIDES.contains(mCachedDevice.getDeviceSide())
-                && mCachedDevice.getBondState() == BOND_BONDED) {
-            mSideToDeviceMap.put(mCachedDevice.getDeviceSide(), mCachedDevice.getDevice());
-            mCachedDevices.add(mCachedDevice);
-        }
-        for (CachedBluetoothDevice memberDevice : mCachedDevice.getMemberDevice()) {
-            if (VALID_SIDES.contains(memberDevice.getDeviceSide())
-                    && memberDevice.getBondState() == BOND_BONDED) {
-                mSideToDeviceMap.put(memberDevice.getDeviceSide(), memberDevice.getDevice());
-                mCachedDevices.add(memberDevice);
-            }
-        }
-        createAmbientVolumePreference();
-        createSliderPreferences();
-        if (mPreference != null) {
-            mPreference.setExpandable(mSideToDeviceMap.size() > 1);
-            mPreference.setSliders((mSideToSliderMap));
-        }
-    }
-
-    private void createAmbientVolumePreference() {
-        if (mPreference != null || mDeviceControls == null) {
-            return;
-        }
-
-        mPreference = new AmbientVolumePreference(mDeviceControls.getContext());
-        mPreference.setKey(KEY_AMBIENT_VOLUME);
-        mPreference.setOrder(ORDER_AMBIENT_VOLUME);
-        mPreference.setOnIconClickListener(
-                new AmbientVolumePreference.OnIconClickListener() {
-                    @Override
-                    public void onExpandIconClick() {
-                        mSideToDeviceMap.forEach((s, d) -> {
-                            if (!isControlMuted()) {
-                                // Apply previous collapsed/expanded volume to remote device
-                                Data data = mLocalDataManager.get(d);
-                                int volume = isControlExpanded()
-                                        ? data.ambient() : data.groupAmbient();
-                                mVolumeController.setAmbient(d, volume);
-                            }
-                            // Update new value to local data
-                            mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded());
-                        });
-                    }
-
-                    @Override
-                    public void onAmbientVolumeIconClick() {
-                        if (!isControlMuted()) {
-                            loadLocalDataToUi();
-                        }
-                        for (BluetoothDevice device : mSideToDeviceMap.values()) {
-                            mVolumeController.setMuted(device, isControlMuted());
-                        }
-                    }
-                });
-        if (mDeviceControls.findPreference(mPreference.getKey()) == null) {
-            mDeviceControls.addPreference(mPreference);
-        }
-    }
-
-    private void createSliderPreferences() {
-        mSideToDeviceMap.forEach((s, d) ->
-                createSliderPreference(s, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + s));
-        createSliderPreference(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED);
-    }
-
-    private void createSliderPreference(int side, int order) {
-        if (mSideToSliderMap.containsKey(side) || mDeviceControls == null) {
-            return;
-        }
-        SeekBarPreference preference = new SeekBarPreference(mDeviceControls.getContext());
-        preference.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side);
-        preference.setOrder(order);
-        preference.setOnPreferenceChangeListener(this);
-        if (side == SIDE_LEFT) {
-            preference.setTitle(mContext.getString(R.string.bluetooth_ambient_volume_control_left));
-        } else if (side == SIDE_RIGHT) {
-            preference.setTitle(
-                    mContext.getString(R.string.bluetooth_ambient_volume_control_right));
-        }
-        mSideToSliderMap.put(side, preference);
-    }
-
-    /** Refreshes the control UI visibility and enabled state. */
-    private void refreshControlUi() {
-        if (mPreference != null) {
-            boolean isAnySliderEnabled = false;
-            for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) {
-                final int side = entry.getKey();
-                final BluetoothDevice device = entry.getValue();
-                final boolean enabled = isDeviceConnectedToVcp(device)
-                        && mVolumeController.isAmbientControlAvailable(device);
-                isAnySliderEnabled |= enabled;
-                mPreference.setSliderEnabled(side, enabled);
-            }
-            mPreference.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled);
-            mPreference.updateLayout();
-        }
-    }
-
-    /** Sets the volume to the corresponding control slider. */
-    private void setVolumeIfValid(int side, int volume) {
-        if (volume == INVALID_VOLUME) {
-            return;
-        }
-        if (mPreference != null) {
-            mPreference.setSliderValue(side, volume);
-        }
-        // Update new value to local data
-        if (side == SIDE_UNIFIED) {
-            mSideToDeviceMap.forEach((s, d) -> mLocalDataManager.updateGroupAmbient(d, volume));
-        } else {
-            mLocalDataManager.updateAmbient(mSideToDeviceMap.get(side), volume);
-        }
-    }
-
-    private void loadLocalDataToUi() {
-        mSideToDeviceMap.forEach((s, d) -> loadLocalDataToUi(d));
-    }
-
-    private void loadLocalDataToUi(BluetoothDevice device) {
-        final Data data = mLocalDataManager.get(device);
-        if (DEBUG) {
-            Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
-        }
-        final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
-        if (isDeviceConnectedToVcp(device) && !isControlMuted()) {
-            setVolumeIfValid(side, data.ambient());
-            setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
-        }
-        setControlExpanded(data.ambientControlExpanded());
-        refreshControlUi();
-    }
-
-    private void loadRemoteDataToUi() {
-        BluetoothDevice leftDevice = mSideToDeviceMap.get(SIDE_LEFT);
-        AmbientVolumeController.RemoteAmbientState leftState =
-                mVolumeController.refreshAmbientState(leftDevice);
-        BluetoothDevice rightDevice = mSideToDeviceMap.get(SIDE_RIGHT);
-        AmbientVolumeController.RemoteAmbientState rightState =
-                mVolumeController.refreshAmbientState(rightDevice);
-        if (DEBUG) {
-            Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState);
-        }
-
-        if (mPreference != null) {
-            mSideToDeviceMap.forEach((side, device) -> {
-                int ambientMax = mVolumeController.getAmbientMax(device);
-                int ambientMin = mVolumeController.getAmbientMin(device);
-                if (ambientMin != ambientMax) {
-                    mPreference.setSliderRange(side, ambientMin, ambientMax);
-                    mPreference.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax);
-                }
-            });
-        }
-
-        // Update ambient volume
-        final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME;
-        final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME;
-        if (isControlExpanded()) {
-            setVolumeIfValid(SIDE_LEFT, leftAmbient);
-            setVolumeIfValid(SIDE_RIGHT, rightAmbient);
-        } else {
-            if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME
-                    && rightAmbient != INVALID_VOLUME) {
-                setVolumeIfValid(SIDE_LEFT, leftAmbient);
-                setVolumeIfValid(SIDE_RIGHT, rightAmbient);
-                setControlExpanded(true);
-            } else {
-                int unifiedAmbient = leftAmbient != INVALID_VOLUME ? leftAmbient : rightAmbient;
-                setVolumeIfValid(SIDE_UNIFIED, unifiedAmbient);
-            }
-        }
-        // Initialize local data between side and group value
-        initLocalDataIfNeeded();
-
-        // Update mute state
-        boolean mutable = true;
-        boolean muted = true;
-        if (isDeviceConnectedToVcp(leftDevice) && leftState != null) {
-            mutable &= leftState.isMutable();
-            muted &= leftState.isMuted();
-        }
-        if (isDeviceConnectedToVcp(rightDevice) && rightState != null) {
-            mutable &= rightState.isMutable();
-            muted &= rightState.isMuted();
-        }
-        if (mPreference != null) {
-            mPreference.setMutable(mutable);
-            mPreference.setMuted(muted);
-        }
-
-        // Ensure remote device mute state is synced
-        syncMuteStateIfNeeded(leftDevice, leftState, muted);
-        syncMuteStateIfNeeded(rightDevice, rightState, muted);
-
-        refreshControlUi();
-    }
-
-    /** Check if any device in the group has valid ambient control points */
-    private boolean isAmbientControlAvailable() {
-        for (BluetoothDevice device : mSideToDeviceMap.values()) {
-            // Found ambient local data for this device, show the ambient control
-            if (mLocalDataManager.get(device).hasAmbientData()) {
-                return true;
-            }
-            // Found remote ambient control points on this device, show the ambient control
-            if (mVolumeController.isAmbientControlAvailable(device)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean isControlExpanded() {
-        return mPreference != null && mPreference.isExpanded();
-    }
-
-    private void setControlExpanded(boolean expanded) {
-        if (mPreference != null && mPreference.isExpanded() != expanded) {
-            mPreference.setExpanded(expanded);
-        }
-        mSideToDeviceMap.forEach((s, d) -> {
-            // Update new value to local data
-            mLocalDataManager.updateAmbientControlExpanded(d, expanded);
-        });
-    }
-
-    private boolean isControlMuted() {
-        return mPreference != null && mPreference.isMuted();
-    }
-
-    private void initLocalDataIfNeeded() {
-        int smallerVolumeAmongGroup = Integer.MAX_VALUE;
-        for (BluetoothDevice device : mSideToDeviceMap.values()) {
-            Data data = mLocalDataManager.get(device);
-            if (data.ambient() != INVALID_VOLUME) {
-                smallerVolumeAmongGroup = Math.min(data.ambient(), smallerVolumeAmongGroup);
-            } else if (data.groupAmbient() != INVALID_VOLUME) {
-                // Initialize side ambient from group ambient value
-                mLocalDataManager.updateAmbient(device, data.groupAmbient());
-            }
-        }
-        if (smallerVolumeAmongGroup != Integer.MAX_VALUE) {
-            for (BluetoothDevice device : mSideToDeviceMap.values()) {
-                Data data = mLocalDataManager.get(device);
-                if (data.groupAmbient() == INVALID_VOLUME) {
-                    // Initialize group ambient from smaller side ambient value
-                    mLocalDataManager.updateGroupAmbient(device, smallerVolumeAmongGroup);
-                }
-            }
-        }
-    }
-
-    private void syncMuteStateIfNeeded(@Nullable BluetoothDevice device,
-            @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted) {
-        if (isDeviceConnectedToVcp(device) && state != null && state.isMutable()) {
-            if (state.isMuted() != muted) {
-                mVolumeController.setMuted(device, muted);
-            }
-        }
-    }
-
-    private boolean isDeviceConnectedToVcp(@Nullable BluetoothDevice device) {
-        return device != null && device.isConnected()
-                && mBluetoothManager.getProfileManager().getVolumeControlProfile()
-                .getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED;
-    }
-
-    private void showErrorToast() {
-        if (mToast != null) {
-            mToast.cancel();
-        }
-        mToast = Toast.makeText(mContext, R.string.bluetooth_ambient_volume_error,
-                Toast.LENGTH_SHORT);
-        mToast.show();
-    }
 }
diff --git a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
index ec406c4..115f642 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
@@ -26,8 +26,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.util.ArrayMap;
 import android.view.View;
@@ -40,6 +42,7 @@
 
 import com.android.settings.R;
 import com.android.settings.widget.SeekBarPreference;
+import com.android.settingslib.bluetooth.AmbientVolumeUi;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -69,14 +72,14 @@
     @Spy
     private Context mContext = ApplicationProvider.getApplicationContext();
     @Mock
-    private AmbientVolumePreference.OnIconClickListener mListener;
+    private AmbientVolumeUi.AmbientVolumeUiListener mListener;
     @Mock
     private View mItemView;
 
     private AmbientVolumePreference mPreference;
     private ImageView mExpandIcon;
     private ImageView mVolumeIcon;
-    private final Map<Integer, SeekBarPreference> mSideToSlidersMap = new ArrayMap<>();
+    private final Map<Integer, BluetoothDevice> mSideToDeviceMap = new ArrayMap<>();
 
     @Before
     public void setUp() {
@@ -84,13 +87,27 @@
         PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
         mPreference = new AmbientVolumePreference(mContext);
         mPreference.setKey(KEY_AMBIENT_VOLUME);
-        mPreference.setOnIconClickListener(mListener);
+        mPreference.setListener(mListener);
         mPreference.setExpandable(true);
         mPreference.setMutable(true);
         preferenceScreen.addPreference(mPreference);
 
-        prepareSliders();
-        mPreference.setSliders(mSideToSlidersMap);
+        prepareDevices();
+        mPreference.setupSliders(mSideToDeviceMap);
+        mPreference.getSliders().forEach((side, slider) -> {
+            slider.setMin(0);
+            slider.setMax(4);
+            if (side == SIDE_LEFT) {
+                slider.setKey(KEY_LEFT_SLIDER);
+                slider.setProgress(TEST_LEFT_VOLUME_LEVEL);
+            } else if (side == SIDE_RIGHT) {
+                slider.setKey(KEY_RIGHT_SLIDER);
+                slider.setProgress(TEST_RIGHT_VOLUME_LEVEL);
+            } else {
+                slider.setKey(KEY_UNIFIED_SLIDER);
+                slider.setProgress(TEST_UNIFIED_VOLUME_LEVEL);
+            }
+        });
 
         mExpandIcon = new ImageView(mContext);
         mVolumeIcon = new ImageView(mContext);
@@ -206,33 +223,16 @@
 
     private void assertControlUiCorrect() {
         final boolean expanded = mPreference.isExpanded();
-        assertThat(mSideToSlidersMap.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded);
-        assertThat(mSideToSlidersMap.get(SIDE_LEFT).isVisible()).isEqualTo(expanded);
-        assertThat(mSideToSlidersMap.get(SIDE_RIGHT).isVisible()).isEqualTo(expanded);
+        Map<Integer, SeekBarPreference> sliders = mPreference.getSliders();
+        assertThat(sliders.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded);
+        assertThat(sliders.get(SIDE_LEFT).isVisible()).isEqualTo(expanded);
+        assertThat(sliders.get(SIDE_RIGHT).isVisible()).isEqualTo(expanded);
         final float rotation = expanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED;
         assertThat(mExpandIcon.getRotation()).isEqualTo(rotation);
     }
 
-    private void prepareSliders() {
-        prepareSlider(SIDE_UNIFIED);
-        prepareSlider(SIDE_LEFT);
-        prepareSlider(SIDE_RIGHT);
-    }
-
-    private void prepareSlider(int side) {
-        SeekBarPreference slider = new SeekBarPreference(mContext);
-        slider.setMin(0);
-        slider.setMax(4);
-        if (side == SIDE_LEFT) {
-            slider.setKey(KEY_LEFT_SLIDER);
-            slider.setProgress(TEST_LEFT_VOLUME_LEVEL);
-        } else if (side == SIDE_RIGHT) {
-            slider.setKey(KEY_RIGHT_SLIDER);
-            slider.setProgress(TEST_RIGHT_VOLUME_LEVEL);
-        } else {
-            slider.setKey(KEY_UNIFIED_SLIDER);
-            slider.setProgress(TEST_UNIFIED_VOLUME_LEVEL);
-        }
-        mSideToSlidersMap.put(side, slider);
+    private void prepareDevices() {
+        mSideToDeviceMap.put(SIDE_LEFT, mock(BluetoothDevice.class));
+        mSideToDeviceMap.put(SIDE_RIGHT, mock(BluetoothDevice.class));
     }
 }
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
index 975d3b4..fb10d09 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
@@ -16,46 +16,25 @@
 
 package com.android.settings.bluetooth;
 
-import static android.bluetooth.AudioInputControl.MUTE_DISABLED;
-import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
-import static android.bluetooth.AudioInputControl.MUTE_MUTED;
-import static android.bluetooth.BluetoothDevice.BOND_BONDED;
-
 import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME;
-import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER;
 import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP;
-import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
-import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.robolectric.Shadows.shadowOf;
 
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.content.ContentResolver;
 import android.os.Handler;
-import android.os.Looper;
-import android.provider.Settings;
 
 import androidx.preference.PreferenceCategory;
 
 import com.android.settings.testutils.shadow.ShadowThreadUtils;
-import com.android.settings.widget.SeekBarPreference;
-import com.android.settingslib.bluetooth.AmbientVolumeController;
+import com.android.settingslib.bluetooth.AmbientVolumeUiController;
 import com.android.settingslib.bluetooth.BluetoothEventManager;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.bluetooth.VolumeControlProfile;
@@ -69,41 +48,19 @@
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.shadows.ShadowSettings;
 
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Executor;
 
 /** Tests for {@link BluetoothDetailsAmbientVolumePreferenceController}. */
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {
-        BluetoothDetailsAmbientVolumePreferenceControllerTest.ShadowGlobal.class,
         ShadowThreadUtils.class
 })
 public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
         BluetoothDetailsControllerTestBase {
-
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    private static final String LEFT_CONTROL_KEY = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_LEFT;
-    private static final String RIGHT_CONTROL_KEY = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_RIGHT;
-    private static final String TEST_ADDRESS = "00:00:00:00:11";
-    private static final String TEST_MEMBER_ADDRESS = "00:00:00:00:22";
-
-    @Mock
-    private CachedBluetoothDevice mCachedMemberDevice;
-    @Mock
-    private BluetoothDevice mDevice;
-    @Mock
-    private BluetoothDevice mMemberDevice;
-    @Mock
-    private HearingDeviceLocalDataManager mLocalDataManager;
     @Mock
     private LocalBluetoothManager mBluetoothManager;
     @Mock
@@ -113,9 +70,9 @@
     @Mock
     private VolumeControlProfile mVolumeControlProfile;
     @Mock
-    private AmbientVolumeController mVolumeController;
-    @Mock
     private Handler mTestHandler;
+    @Mock
+    private AmbientVolumeUiController mUiController;
 
     private BluetoothDetailsAmbientVolumePreferenceController mController;
 
@@ -124,24 +81,16 @@
         super.setUp();
 
         mContext = spy(mContext);
+
+        when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mBluetoothManager.getEventManager()).thenReturn(mEventManager);
+        mController = spy(
+                new BluetoothDetailsAmbientVolumePreferenceController(mContext, mBluetoothManager,
+                        mFragment, mCachedDevice, mLifecycle, mUiController));
+
         PreferenceCategory deviceControls = new PreferenceCategory(mContext);
         deviceControls.setKey(KEY_HEARING_DEVICE_GROUP);
         mScreen.addPreference(deviceControls);
-        mController = spy(
-                new BluetoothDetailsAmbientVolumePreferenceController(mContext, mBluetoothManager,
-                        mFragment, mCachedDevice, mLifecycle, mLocalDataManager,
-                        mVolumeController));
-
-        when(mBluetoothManager.getEventManager()).thenReturn(mEventManager);
-        when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
-        when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile);
-        when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn(
-                BluetoothProfile.STATE_CONNECTED);
-        when(mVolumeControlProfile.getConnectionStatus(mMemberDevice)).thenReturn(
-                BluetoothProfile.STATE_CONNECTED);
-        when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile));
-        when(mLocalDataManager.get(any(BluetoothDevice.class))).thenReturn(
-                new HearingDeviceLocalDataManager.Data.Builder().build());
 
         when(mContext.getMainThreadHandler()).thenReturn(mTestHandler);
         when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
@@ -152,283 +101,42 @@
     }
 
     @Test
-    public void init_deviceWithoutMember_controlNotExpandable() {
-        prepareDevice(/* hasMember= */ false);
-
+    public void init_preferenceAdded() {
         mController.init(mScreen);
 
         AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
         assertThat(preference).isNotNull();
-        assertThat(preference.isExpandable()).isFalse();
     }
 
     @Test
-    public void init_deviceWithMember_controlExpandable() {
-        prepareDevice(/* hasMember= */ true);
+    public void refresh_deviceNotSupportVcp_verifyUiControllerNoRefresh() {
+        when(mCachedDevice.getProfiles()).thenReturn(List.of());
 
-        mController.init(mScreen);
+        mController.refresh();
 
-        AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
-        assertThat(preference).isNotNull();
-        assertThat(preference.isExpandable()).isTrue();
+        verify(mUiController, never()).refresh();
     }
 
     @Test
-    public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() {
-        prepareDevice(/* hasMember= */ false);
-        mController.init(mScreen);
-        HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
-                .ambient(0).groupAmbient(0).ambientControlExpanded(true).build();
-        when(mLocalDataManager.get(mDevice)).thenReturn(data);
+    public void refresh_deviceSupportVcp_verifyUiControllerRefresh() {
+        when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile));
 
-        mController.onDeviceLocalDataChange(TEST_ADDRESS, data);
-        shadowOf(Looper.getMainLooper()).idle();
+        mController.refresh();
 
-        AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
-        assertThat(preference).isNotNull();
-        assertThat(preference.isExpanded()).isFalse();
-        verifyDeviceDataUpdated(mDevice);
+        verify(mUiController).refresh();
     }
 
     @Test
-    public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() {
-        prepareDevice(/* hasMember= */ false);
-        mController.init(mScreen);
-        HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
-                .ambient(0).groupAmbient(0).ambientControlExpanded(false).build();
-        when(mLocalDataManager.get(mDevice)).thenReturn(data);
-
-        mController.onDeviceLocalDataChange(TEST_ADDRESS, data);
-        shadowOf(Looper.getMainLooper()).idle();
-
-        AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
-        assertThat(preference).isNotNull();
-        assertThat(preference.isExpanded()).isFalse();
-        verifyDeviceDataUpdated(mDevice);
-    }
-
-    @Test
-    public void onDeviceLocalDataChange_hasMemberAndExpanded_uiCorrectAndDataUpdated() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-        HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
-                .ambient(0).groupAmbient(0).ambientControlExpanded(true).build();
-        when(mLocalDataManager.get(mDevice)).thenReturn(data);
-
-        mController.onDeviceLocalDataChange(TEST_ADDRESS, data);
-        shadowOf(Looper.getMainLooper()).idle();
-
-        AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
-        assertThat(preference).isNotNull();
-        assertThat(preference.isExpanded()).isTrue();
-        verifyDeviceDataUpdated(mDevice);
-    }
-
-    @Test
-    public void onDeviceLocalDataChange_hasMemberAndCollapsed_uiCorrectAndDataUpdated() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-        HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
-                .ambient(0).groupAmbient(0).ambientControlExpanded(false).build();
-        when(mLocalDataManager.get(mDevice)).thenReturn(data);
-
-        mController.onDeviceLocalDataChange(TEST_ADDRESS, data);
-        shadowOf(Looper.getMainLooper()).idle();
-
-        AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
-        assertThat(preference).isNotNull();
-        assertThat(preference.isExpanded()).isFalse();
-        verifyDeviceDataUpdated(mDevice);
-    }
-
-    @Test
-    public void onStart_localDataManagerStartAndCallbackRegistered() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-
+    public void onStart_verifyUiControllerStart() {
         mController.onStart();
 
-        verify(mLocalDataManager, atLeastOnce()).start();
-        verify(mVolumeController).registerCallback(any(Executor.class), eq(mDevice));
-        verify(mVolumeController).registerCallback(any(Executor.class), eq(mMemberDevice));
-        verify(mCachedDevice).registerCallback(any(Executor.class),
-                any(CachedBluetoothDevice.Callback.class));
-        verify(mCachedMemberDevice).registerCallback(any(Executor.class),
-                any(CachedBluetoothDevice.Callback.class));
+        verify(mUiController).start();
     }
 
     @Test
-    public void onStop_localDataManagerStopAndCallbackUnregistered() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-
+    public void onStop_verifyUiControllerStop() {
         mController.onStop();
 
-        verify(mLocalDataManager).stop();
-        verify(mVolumeController).unregisterCallback(mDevice);
-        verify(mVolumeController).unregisterCallback(mMemberDevice);
-        verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class));
-        verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class));
-    }
-
-    @Test
-    public void onDeviceAttributesChanged_newDevice_newPreference() {
-        prepareDevice(/* hasMember= */ false);
-        mController.init(mScreen);
-
-        // check the right control is null before onDeviceAttributesChanged()
-        SeekBarPreference leftControl = mScreen.findPreference(LEFT_CONTROL_KEY);
-        SeekBarPreference rightControl = mScreen.findPreference(RIGHT_CONTROL_KEY);
-        assertThat(leftControl).isNotNull();
-        assertThat(rightControl).isNull();
-
-        prepareDevice(/* hasMember= */ true);
-        mController.onDeviceAttributesChanged();
-        shadowOf(Looper.getMainLooper()).idle();
-
-        // check the right control is created after onDeviceAttributesChanged()
-        SeekBarPreference updatedLeftControl = mScreen.findPreference(LEFT_CONTROL_KEY);
-        SeekBarPreference updatedRightControl = mScreen.findPreference(RIGHT_CONTROL_KEY);
-        assertThat(updatedLeftControl).isEqualTo(leftControl);
-        assertThat(updatedRightControl).isNotNull();
-    }
-
-    @Test
-    public void onAmbientChanged_refreshWhenNotInitiateFromUi() {
-        prepareDevice(/* hasMember= */ false);
-        mController.init(mScreen);
-        final int testAmbient = 10;
-        HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
-                .ambient(testAmbient)
-                .groupAmbient(testAmbient)
-                .ambientControlExpanded(false)
-                .build();
-        when(mLocalDataManager.get(mDevice)).thenReturn(data);
-        getPreference().setExpanded(true);
-
-        mController.onAmbientChanged(mDevice, testAmbient);
-        verify(mController, never()).refresh();
-
-        final int updatedTestAmbient = 20;
-        mController.onAmbientChanged(mDevice, updatedTestAmbient);
-        verify(mController).refresh();
-    }
-
-    @Test
-    public void onMuteChanged_refreshWhenNotInitiateFromUi() {
-        prepareDevice(/* hasMember= */ false);
-        mController.init(mScreen);
-        final int testMute = MUTE_NOT_MUTED;
-        AmbientVolumeController.RemoteAmbientState state =
-                new AmbientVolumeController.RemoteAmbientState(testMute, 0);
-        when(mVolumeController.refreshAmbientState(mDevice)).thenReturn(state);
-        getPreference().setMuted(false);
-
-        mController.onMuteChanged(mDevice, testMute);
-        verify(mController, never()).refresh();
-
-        final int updatedTestMute = MUTE_MUTED;
-        mController.onMuteChanged(mDevice, updatedTestMute);
-        verify(mController).refresh();
-    }
-
-    @Test
-    public void refresh_leftAndRightDifferentGainSetting_expandControl() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-        prepareRemoteData(mDevice, 10, MUTE_NOT_MUTED);
-        prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
-        getPreference().setExpanded(false);
-
-        mController.refresh();
-
-        assertThat(getPreference().isExpanded()).isTrue();
-    }
-
-    @Test
-    public void refresh_oneSideNotMutable_controlNotMutableAndNotMuted() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-        prepareRemoteData(mDevice, 10, MUTE_DISABLED);
-        prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
-        getPreference().setMutable(true);
-        getPreference().setMuted(true);
-
-        mController.refresh();
-
-        assertThat(getPreference().isMutable()).isFalse();
-        assertThat(getPreference().isMuted()).isFalse();
-    }
-
-    @Test
-    public void refresh_oneSideNotMuted_controlNotMutedAndSyncToRemote() {
-        prepareDevice(/* hasMember= */ true);
-        mController.init(mScreen);
-        prepareRemoteData(mDevice, 10, MUTE_MUTED);
-        prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
-        getPreference().setMutable(true);
-        getPreference().setMuted(true);
-
-        mController.refresh();
-
-        assertThat(getPreference().isMutable()).isTrue();
-        assertThat(getPreference().isMuted()).isFalse();
-        verify(mVolumeController).setMuted(mDevice, false);
-    }
-
-    private void prepareDevice(boolean hasMember) {
-        when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT);
-        when(mCachedDevice.getDevice()).thenReturn(mDevice);
-        when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED);
-        when(mDevice.getAddress()).thenReturn(TEST_ADDRESS);
-        when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS);
-        when(mDevice.isConnected()).thenReturn(true);
-        if (hasMember) {
-            when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice));
-            when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT);
-            when(mCachedMemberDevice.getDevice()).thenReturn(mMemberDevice);
-            when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED);
-            when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS);
-            when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS);
-            when(mMemberDevice.isConnected()).thenReturn(true);
-        }
-    }
-
-    private void prepareRemoteData(BluetoothDevice device, int gainSetting, int mute) {
-        when(mVolumeController.isAmbientControlAvailable(device)).thenReturn(true);
-        when(mVolumeController.refreshAmbientState(device)).thenReturn(
-                new AmbientVolumeController.RemoteAmbientState(gainSetting, mute));
-    }
-
-    private void verifyDeviceDataUpdated(BluetoothDevice device) {
-        verify(mLocalDataManager, atLeastOnce()).updateAmbient(eq(device), anyInt());
-        verify(mLocalDataManager, atLeastOnce()).updateGroupAmbient(eq(device), anyInt());
-        verify(mLocalDataManager, atLeastOnce()).updateAmbientControlExpanded(eq(device),
-                anyBoolean());
-    }
-
-    private AmbientVolumePreference getPreference() {
-        return mScreen.findPreference(KEY_AMBIENT_VOLUME);
-    }
-
-    @Implements(value = Settings.Global.class)
-    public static class ShadowGlobal extends ShadowSettings.ShadowGlobal {
-        private static final Map<ContentResolver, Map<String, String>> sDataMap = new HashMap<>();
-
-        @Implementation
-        protected static boolean putStringForUser(
-                ContentResolver cr, String name, String value, int userHandle) {
-            get(cr).put(name, value);
-            return true;
-        }
-
-        @Implementation
-        protected static String getStringForUser(ContentResolver cr, String name, int userHandle) {
-            return get(cr).get(name);
-        }
-
-        private static Map<String, String> get(ContentResolver cr) {
-            return sDataMap.computeIfAbsent(cr, k -> new HashMap<>());
-        }
+        verify(mUiController).stop();
     }
 }