[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
This commit is contained in:
Angela Wang
2024-11-11 07:23:46 +00:00
parent c2ca7dadd9
commit 46537a6576
4 changed files with 315 additions and 36 deletions

View File

@@ -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);
setVolumeIfValid(side, data.ambient());
setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
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();
}
}