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

Commit 5ec31aff authored by Ze Li's avatar Ze Li
Browse files

[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
parent aadb59bb
Loading
Loading
Loading
Loading
+167 −20
Original line number Diff line number Diff line
@@ -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,21 +1217,147 @@ 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 */,
        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);
        if (summary != null) {
            return summary.toString();
        }
        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.
     */
@@ -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;
    }
}
+112 −0
Original line number Diff line number Diff line
@@ -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)