Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 46537a65 authored by Angela Wang's avatar Angela Wang
Browse files

[Ambient Volume] Show value with remote data

Sync local data with remote data when UI need to refresh and set the
corresponding local value to remote when the control expanded/collapsed.

Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control
Bug: 357878944
Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest
Change-Id: If748e696eb62b199d4fd9abafa2300d301a8079c
parent c2ca7dad
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -174,6 +174,8 @@
    <string name="bluetooth_ambient_volume_control_left">Left</string>
    <!-- Connected devices settings. The text to show the control is for right side device. [CHAR LIMIT=30] -->
    <string name="bluetooth_ambient_volume_control_right">Right</string>
    <!-- Message when changing ambient state failed. [CHAR LIMIT=NONE] -->
    <string name="bluetooth_ambient_volume_error">Couldn\u2019t update surroundings</string>
    <!-- Connected devices settings. Title of the preference to show the entrance of the audio output page. It can change different types of audio are played on phone or other bluetooth devices. [CHAR LIMIT=35] -->
    <string name="bluetooth_audio_routing_title">Audio output</string>
    <!-- Title for bluetooth audio routing page footer. [CHAR LIMIT=30] -->
+214 −8
Original line number Diff line number Diff line
@@ -28,9 +28,11 @@ import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_R
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;
@@ -42,9 +44,12 @@ 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.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;
import com.android.settingslib.core.lifecycle.events.OnStart;
@@ -54,12 +59,14 @@ 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 {
        HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop,
        AmbientVolumeController.AmbientVolumeControlCallback, BluetoothCallback {

    private static final boolean DEBUG = true;
    private static final String TAG = "AmbientPrefController";
@@ -69,34 +76,45 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
    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;

    public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
            @NonNull LocalBluetoothManager manager,
            @NonNull PreferenceFragmentCompat fragment,
            @NonNull CachedBluetoothDevice device,
            @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,
            @NonNull LocalBluetoothManager manager,
            @NonNull PreferenceFragmentCompat fragment,
            @NonNull CachedBluetoothDevice device,
            @NonNull Lifecycle lifecycle,
            @NonNull HearingDeviceLocalDataManager localSettings) {
            @NonNull HearingDeviceLocalDataManager localSettings,
            @NonNull AmbientVolumeController volumeController) {
        super(context, fragment, device, lifecycle);
        mBluetoothManager = manager;
        mLocalDataManager = localSettings;
        mVolumeController = volumeController;
    }

    @Override
@@ -111,19 +129,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
    @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());
            });
        });
    }

    @Override
    public void onResume() {
        refresh();
    }

    @Override
    public void onPause() {
    }

    @Override
    public void onStop() {
        ThreadUtils.postOnBackgroundThread(() -> {
            mBluetoothManager.getEventManager().unregisterCallback(this);
            mLocalDataManager.stop();
            mCachedDevices.forEach(device -> {
                device.unregisterCallback(this);
                mVolumeController.unregisterCallback(device.getDevice());
            });
        });
    }
@@ -133,8 +165,17 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
        if (!isAvailable()) {
            return;
        }
        // TODO: load data from remote
        loadLocalDataToUi();
        boolean shouldShowAmbientControl = isAmbientControlAvailable();
        if (shouldShowAmbientControl) {
            if (mPreference != null) {
                mPreference.setVisible(true);
            }
            loadRemoteDataToUi();
        } else {
            if (mPreference != null) {
                mPreference.setVisible(false);
            }
        }
    }

    @Override
@@ -160,19 +201,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
            setVolumeIfValid(side, value);

            if (side == SIDE_UNIFIED) {
                // TODO: set the value on the devices
                mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
            } else {
                // TODO: set the value on the side device
                final BluetoothDevice device = mSideToDeviceMap.get(side);
                mVolumeController.setAmbient(device, value);
            }
            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();
@@ -182,6 +237,8 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
            ThreadUtils.postOnBackgroundThread(() ->
                    mCachedDevices.forEach(device -> {
                        device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
                        mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
                                device.getDevice());
                    })
            );
        });
@@ -201,6 +258,41 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
        }
    }

    @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 onCommandFailed(@NonNull BluetoothDevice device) {
        Log.w(TAG, "onCommandFailed, device:" + device);
        mContext.getMainExecutor().execute(() -> {
            showErrorToast();
            refresh();
        });
    }

    private void loadDevices() {
        mSideToDeviceMap.clear();
        mCachedDevices.clear();
@@ -234,6 +326,11 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
        mPreference.setOrder(ORDER_AMBIENT_VOLUME);
        mPreference.setOnIconClickListener(() -> {
            mSideToDeviceMap.forEach((s, d) -> {
                // 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());
            });
@@ -269,6 +366,16 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
    /** 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();
        }
    }
@@ -299,12 +406,74 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
            Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
        }
        final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
        if (isDeviceConnectedToVcp(device)) {
            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();

        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();
    }
@@ -318,4 +487,41 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
            mLocalDataManager.updateAmbientControlExpanded(d, expanded);
        });
    }

    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 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();
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -110,7 +110,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon
        }
        if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) {
            mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext,
                    mFragment, mCachedDevice, mLifecycle));
                    mManager, mFragment, mCachedDevice, mLifecycle));
        }
    }

+98 −27

File changed.

Preview size limit exceeded, changes collapsed.