[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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user