summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ze Li <lze@google.com> 2024-04-11 18:07:10 +0800
committer Ze Li <lze@google.com> 2024-04-19 17:19:13 +0800
commit5ec31aff247498addb661e5d318c2021a588eb55 (patch)
tree778b34efdd60e6bbd70787f6ff8208013a438b40
parentaadb59bb389fc03ffbfde8d0e4062399670f209d (diff)
[Audiosharing] Update device summary for CachedBluetoothDevice during audio sharing
Bug: 331152872 Flag: com.android.settingslib.flags.Flags.enableLeAudioSharing Test: manual: com.android.settingslib.bluetooth.CachedBluetoothDeviceTest Change-Id: I99d8ca8b135ff1461b915ef19f48a60b32efe87f
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java187
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java112
2 files changed, 279 insertions, 20 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 04516eba250e..36a9ecfe99f5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -19,6 +19,7 @@ package com.android.settingslib.bluetooth;
import static com.android.settingslib.flags.Flags.enableSetPreferredTransportForLeAudioDevice;
import android.annotation.CallbackExecutor;
+import android.annotation.StringRes;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -37,6 +38,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.SystemClock;
+import android.provider.Settings;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
@@ -45,6 +47,7 @@ import android.util.LruCache;
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -102,6 +105,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private HearingAidInfo mHearingAidInfo;
private int mGroupId;
private Timestamp mBondTimestamp;
+ private LocalBluetoothManager mBluetoothManager;
// Need this since there is no method for getting RSSI
short mRssi;
@@ -722,6 +726,25 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
.orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
}
+ /**
+ * Get the lowest battery level from remote device and its member devices if it's greater than
+ * BluetoothDevice.BATTERY_LEVEL_UNKNOWN.
+ *
+ * <p>Android framework should only set mBatteryLevel to valid range [0-100],
+ * BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any
+ * other value should be a framework bug. Thus assume here that if value is greater than
+ * BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid
+ *
+ * @return battery level in String [0-100] or Null if this lower than
+ * BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ */
+ @Nullable
+ private String getValidMinBatteryLevelWithMemberDevices() {
+ final int batteryLevel = getMinBatteryLevelWithMemberDevices();
+ return batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+ ? com.android.settingslib.Utils.formatPercentage(batteryLevel)
+ : null;
+ }
void refresh() {
ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
@@ -1194,22 +1217,148 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
/**
- * Return summary that describes connection state of this device. Summary depends on:
- * 1. Whether device has battery info
- * 2. Whether device is in active usage(or in phone call)
+ * Return summary that describes connection state of this device. Summary depends on: 1. Whether
+ * device has battery info 2. Whether device is in active usage(or in phone call) 3. Whether
+ * device is in audio sharing process
*
* @param shortSummary {@code true} if need to return short version summary
*/
public String getConnectionSummary(boolean shortSummary) {
- CharSequence summary = getConnectionSummary(shortSummary, false /* isTvSummary */,
- SUMMARY_NO_COLOR_FOR_LOW_BATTERY);
- if (summary != null) {
- return summary.toString();
+ CharSequence summary = null;
+ if (BluetoothUtils.isAudioSharingEnabled()) {
+ if (mBluetoothManager == null) {
+ mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
+ }
+ if (BluetoothUtils.isBroadcasting(mBluetoothManager)) {
+ summary = getBroadcastConnectionSummary(shortSummary);
+ }
+ }
+ if (summary == null) {
+ summary =
+ getConnectionSummary(
+ shortSummary,
+ false /* isTvSummary */,
+ SUMMARY_NO_COLOR_FOR_LOW_BATTERY);
+ }
+ return summary != null ? summary.toString() : null;
+ }
+
+ /**
+ * Returns the connection summary of this device during le audio sharing.
+ *
+ * @param shortSummary {@code true} if need to return short version summary
+ */
+ @Nullable
+ private String getBroadcastConnectionSummary(boolean shortSummary) {
+ if (isProfileConnectedFail() && isConnected()) {
+ return mContext.getString(R.string.profile_connect_timeout_subtext);
+ }
+
+ synchronized (mProfileLock) {
+ for (LocalBluetoothProfile profile : getProfiles()) {
+ int connectionStatus = getProfileConnectionState(profile);
+ if (connectionStatus == BluetoothProfile.STATE_CONNECTING
+ || connectionStatus == BluetoothProfile.STATE_DISCONNECTING) {
+ return mContext.getString(
+ BluetoothUtils.getConnectionStateSummary(connectionStatus));
+ }
+ }
+ }
+
+ int leftBattery =
+ BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
+ int rightBattery =
+ BluetoothUtils.getIntMetaData(
+ mDevice, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
+ String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices();
+
+ if (mBluetoothManager == null) {
+ mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
+ }
+ if (BluetoothUtils.hasConnectedBroadcastSource(this, mBluetoothManager)) {
+ // Gets summary for the buds which are in the audio sharing.
+ int groupId = BluetoothUtils.getGroupId(this);
+ if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+ && groupId
+ == Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ "bluetooth_le_broadcast_fallback_active_group_id",
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
+ // The buds are primary buds
+ return getSummaryWithBatteryInfo(
+ R.string.bluetooth_active_battery_level_untethered,
+ R.string.bluetooth_active_battery_level,
+ R.string.bluetooth_active_no_battery_level,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary);
+ } else {
+ // The buds are not primary buds
+ return getSummaryWithBatteryInfo(
+ R.string.bluetooth_active_media_only_battery_level_untethered,
+ R.string.bluetooth_active_media_only_battery_level,
+ R.string.bluetooth_active_media_only_no_battery_level,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary);
+ }
+ } else {
+ // Gets summary for the buds which are not in the audio sharing.
+ if (getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof LeAudioProfile
+ && profile.isEnabled(getDevice()))) {
+ // The buds support le audio.
+ if (isConnected()) {
+ return getSummaryWithBatteryInfo(
+ R.string.bluetooth_battery_level_untethered_lea_support,
+ R.string.bluetooth_battery_level_lea_support,
+ R.string.bluetooth_no_battery_level_lea_support,
+ leftBattery,
+ rightBattery,
+ batteryLevelPercentageString,
+ shortSummary);
+ } else {
+ return mContext.getString(R.string.bluetooth_saved_device_lea_support);
+ }
+ }
}
return null;
}
/**
+ * Returns the summary with correct format depending the battery info.
+ *
+ * @param untetheredBatteryResId resource id for untethered device with battery info
+ * @param batteryResId resource id for device with single battery info
+ * @param noBatteryResId resource id for device with no battery info
+ * @param shortSummary {@code true} if need to return short version summary
+ */
+ private String getSummaryWithBatteryInfo(
+ @StringRes int untetheredBatteryResId,
+ @StringRes int batteryResId,
+ @StringRes int noBatteryResId,
+ int leftBattery,
+ int rightBattery,
+ String batteryLevelPercentageString,
+ boolean shortSummary) {
+ if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) {
+ return mContext.getString(
+ untetheredBatteryResId,
+ Utils.formatPercentage(leftBattery),
+ Utils.formatPercentage(rightBattery));
+ } else if (batteryLevelPercentageString != null && !shortSummary) {
+ return mContext.getString(batteryResId, batteryLevelPercentageString);
+ } else {
+ return mContext.getString(noBatteryResId);
+ }
+ }
+
+ /**
* Returns android tv string that describes the connection state of this device.
*/
public CharSequence getTvConnectionSummary() {
@@ -1286,18 +1435,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
}
- String batteryLevelPercentageString = null;
- // Android framework should only set mBatteryLevel to valid range [0-100],
- // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
- // any other value should be a framework bug. Thus assume here that if value is greater
- // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid
- final int batteryLevel = getMinBatteryLevelWithMemberDevices();
- if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
- // TODO: name com.android.settingslib.bluetooth.Utils something different
- batteryLevelPercentageString =
- com.android.settingslib.Utils.formatPercentage(batteryLevel);
- }
-
+ String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices();
int stringRes = R.string.bluetooth_pairing;
//when profile is connected, information would be available
if (profileConnected) {
@@ -1376,7 +1514,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
|| stringRes == R.string.bluetooth_active_battery_level_untethered
|| stringRes == R.string.bluetooth_battery_level_untethered;
if (isTvSummary && summaryIncludesBatteryLevel && Flags.enableTvMediaOutputDialog()) {
- return getTvBatterySummary(batteryLevel, leftBattery, rightBattery, lowBatteryColorRes);
+ return getTvBatterySummary(
+ getMinBatteryLevelWithMemberDevices(),
+ leftBattery,
+ rightBattery,
+ lowBatteryColorRes);
}
if (isTwsBatteryAvailable(leftBattery, rightBattery)) {
@@ -1793,4 +1935,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
boolean getUnpairing() {
return mUnpairing;
}
+
+ @VisibleForTesting
+ void setLocalBluetoothManager(LocalBluetoothManager bluetoothManager) {
+ mBluetoothManager = bluetoothManager;
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 646e9ebd4f09..1b5d3a39713b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -15,6 +15,7 @@
*/
package com.android.settingslib.bluetooth;
+import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE;
import static com.google.common.truth.Truth.assertThat;
@@ -30,14 +31,17 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
import android.util.LruCache;
@@ -47,6 +51,8 @@ import com.android.settingslib.media.flags.Flags;
import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
+import com.google.common.collect.ImmutableList;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -58,6 +64,9 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class CachedBluetoothDeviceTest {
@@ -95,6 +104,14 @@ public class CachedBluetoothDeviceTest {
private BluetoothDevice mDevice;
@Mock
private BluetoothDevice mSubDevice;
+ @Mock
+ private LocalBluetoothLeBroadcast mBroadcast;
+ @Mock
+ private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock
+ private LocalBluetoothLeBroadcastAssistant mAssistant;
+ @Mock
+ private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState;
private CachedBluetoothDevice mCachedDevice;
private CachedBluetoothDevice mSubCachedDevice;
private AudioManager mAudioManager;
@@ -110,9 +127,14 @@ public class CachedBluetoothDeviceTest {
MockitoAnnotations.initMocks(this);
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG);
mSetFlagsRule.enableFlags(FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE);
+ mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
mContext = RuntimeEnvironment.application;
mAudioManager = mContext.getSystemService(AudioManager.class);
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
when(mHfpProfile.isProfileReady()).thenReturn(true);
when(mHfpProfile.getProfileId()).thenReturn(BluetoothProfile.HEADSET);
@@ -126,7 +148,12 @@ public class CachedBluetoothDeviceTest {
when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO);
when(mHidProfile.isProfileReady()).thenReturn(true);
when(mHidProfile.getProfileId()).thenReturn(BluetoothProfile.HID_HOST);
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
+ when(mBroadcast.isEnabled(any())).thenReturn(false);
+ when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+ when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice));
+ mCachedDevice.setLocalBluetoothManager(mLocalBluetoothManager);
mSubCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mSubDevice));
doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel();
doAnswer((invocation) -> mBatteryLevel).when(mSubCachedDevice).getBatteryLevel();
@@ -1853,6 +1880,91 @@ public class CachedBluetoothDeviceTest {
verify(mHidProfile).setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_BREDR);
}
+ @Test
+ public void getConnectionSummary_isBroadcastPrimary_returnActive() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(),
+ "bluetooth_le_broadcast_fallback_active_group_id",
+ 1);
+
+ List<Long> bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+ sourceList.add(mLeBroadcastReceiveState);
+ when(mAssistant.getAllSources(any())).thenReturn(sourceList);
+
+ when(mCachedDevice.getGroupId())
+ .thenReturn(
+ Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ "bluetooth_le_broadcast_fallback_active_group_id",
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID));
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level));
+ }
+
+ @Test
+ public void getConnectionSummary_isBroadcastNotPrimary_returnActiveMedia() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(),
+ "bluetooth_le_broadcast_fallback_active_group_id",
+ 1);
+
+ List<Long> bisSyncState = new ArrayList<>();
+ bisSyncState.add(1L);
+ when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>();
+ sourceList.add(mLeBroadcastReceiveState);
+ when(mAssistant.getAllSources(any())).thenReturn(sourceList);
+
+ when(mCachedDevice.getGroupId()).thenReturn(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(
+ mContext.getString(R.string.bluetooth_active_media_only_no_battery_level));
+ }
+
+ @Test
+ public void getConnectionSummary_supportBroadcastConnected_returnConnectedSupportLe() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+ when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedDevice.isConnected()).thenReturn(true);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_no_battery_level_lea_support));
+ }
+
+ @Test
+ public void getConnectionSummary_supportBroadcastNotConnected_returnSupportLe() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+ when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedDevice.isConnected()).thenReturn(false);
+
+ assertThat(mCachedDevice.getConnectionSummary(false))
+ .isEqualTo(mContext.getString(R.string.bluetooth_saved_device_lea_support));
+ }
+
+ @Test
+ public void getConnectionSummary_doNotSupportBroadcast_returnNull() {
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+
+ when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of());
+
+ assertThat(mCachedDevice.getConnectionSummary(false)).isNull();
+ }
+
private HearingAidInfo getLeftAshaHearingAidInfo() {
return new HearingAidInfo.Builder()
.setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT)