diff options
| author | 2024-04-11 18:07:10 +0800 | |
|---|---|---|
| committer | 2024-04-19 17:19:13 +0800 | |
| commit | 5ec31aff247498addb661e5d318c2021a588eb55 (patch) | |
| tree | 778b34efdd60e6bbd70787f6ff8208013a438b40 | |
| parent | aadb59bb389fc03ffbfde8d0e4062399670f209d (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
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) |