Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +40 −8 Original line number Diff line number Diff line Loading @@ -444,12 +444,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } /** * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device. * @deprecated use {@link #isHearingDevice() } * // TODO: b/385679160 - Target to deprecate it and replace with #isHearingDevice() */ @Deprecated public boolean isHearingAidDevice() { return mHearingAidInfo != null; } /** * @return {@code true} if {@code cachedBluetoothDevice} support any of hearing device profile. */ public boolean isHearingDevice() { return getProfiles().stream().anyMatch( p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile)); } public int getDeviceSide() { return mHearingAidInfo != null ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID; Loading Loading @@ -910,12 +921,33 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } /** * Checks if the device is connected to the specified Bluetooth profile. * * @param profile The Bluetooth profile to check. * @return {@code true} if the device is connected to the profile. */ public boolean isConnectedProfile(LocalBluetoothProfile profile) { int status = getProfileConnectionState(profile); return status == BluetoothProfile.STATE_CONNECTED; } /** * Checks if the device is connected to the Bluetooth profile with the given ID. * * @param profileId The ID of the Bluetooth profile to check. * @return {@code true} if the device is connected to the profile. */ public boolean isConnectedProfile(int profileId) { for (LocalBluetoothProfile profile : getProfiles()) { if (profile.getProfileId() == profileId) { return isConnectedProfile(profile); } } return false; } public boolean isBusy() { synchronized (mProfileLock) { for (LocalBluetoothProfile profile : mProfiles) { Loading Loading @@ -1890,13 +1922,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> == BluetoothProfile.STATE_CONNECTED; } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device */ public boolean isConnectedLeAudioHearingAidDevice() { return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device * Loading @@ -1907,6 +1932,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device */ public boolean isConnectedLeAudioHearingAidDevice() { return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device */ Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +33 −2 Original line number Diff line number Diff line Loading @@ -24,7 +24,10 @@ import android.bluetooth.le.ScanFilter; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.flags.Flags; import java.sql.Timestamp; import java.util.ArrayList; Loading @@ -46,7 +49,7 @@ public class CachedBluetoothDeviceManager { private final LocalBluetoothManager mBtManager; @VisibleForTesting final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>(); @VisibleForTesting HearingAidDeviceManager mHearingAidDeviceManager; @VisibleForTesting Loading Loading @@ -191,6 +194,20 @@ public class CachedBluetoothDeviceManager { } } /** * Notifies the connection status if device is hearing device. * * @param device The {@link CachedBluetoothDevice} need to be hearing device */ public synchronized void notifyHearingDevicesConnectionStatusChangedIfNeeded( @NonNull CachedBluetoothDevice device) { if (!device.isHearingDevice()) { return; } mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } /** * Search for existing sub device {@link CachedBluetoothDevice}. * Loading Loading @@ -388,8 +405,14 @@ public class CachedBluetoothDeviceManager { /** Handles when the device been set as active/inactive. */ public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) { if (cachedBluetoothDevice.isHearingAidDevice()) { if (cachedBluetoothDevice == null) { return; } if (cachedBluetoothDevice.isHearingDevice()) { mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice); if (Flags.hearingDeviceSetConnectionStatusReport()) { mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } } } Loading Loading @@ -421,6 +444,14 @@ public class CachedBluetoothDeviceManager { mainDevice.unpair(); mainDevice.setSubDevice(null); } // TODO: b/386121967 - Should change to use isHearingDevice but mProfile get clear here. // Need to consider where to put this logic when using isHearingDevice() if (device.isHearingAidDevice()) { if (Flags.hearingDeviceSetConnectionStatusReport()) { mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } } } /** Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +154 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,10 @@ */ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_BONDED; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; Loading @@ -30,15 +33,22 @@ import android.provider.Settings; import android.util.FeatureFlagUtils; import android.util.Log; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.collection.ArraySet; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices. * HearingAidDeviceManager manages the set of remote bluetooth hearing devices. */ public class HearingAidDeviceManager { private static final String TAG = "HearingAidDeviceManager"; Loading @@ -49,6 +59,10 @@ public class HearingAidDeviceManager { private final LocalBluetoothManager mBtManager; private final List<CachedBluetoothDevice> mCachedDevices; private final HearingAidAudioRoutingHelper mRoutingHelper; @ConnectionStatus private int mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; private boolean mInitialDevicesConnectionStatusUpdate = false; HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices) { mContext = context; Loading @@ -68,6 +82,145 @@ public class HearingAidDeviceManager { mRoutingHelper = routingHelper; } /** * Defines the connection status for hearing devices. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ ConnectionStatus.NO_DEVICE_BONDED, ConnectionStatus.DISCONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTING_OR_DISCONNECTING, ConnectionStatus.ACTIVE }) public @interface ConnectionStatus { int NO_DEVICE_BONDED = -1; int DISCONNECTED = 0; int CONNECTED = 1; int CONNECTING_OR_DISCONNECTING = 2; int ACTIVE = 3; } /** * Updates the connection status of the hearing devices based on the currently bonded * hearing aid devices. */ synchronized void notifyDevicesConnectionStatusChanged() { updateDevicesConnectionStatus(); // TODO: b/357882387 - notify connection status changes for the callers } private void updateDevicesConnectionStatus() { mInitialDevicesConnectionStatusUpdate = true; // Add all hearing devices including sub and member into a set. Set<CachedBluetoothDevice> allHearingDevices = mCachedDevices.stream() .filter(d -> d.getBondState() == BluetoothDevice.BOND_BONDED && d.isHearingDevice()) .flatMap(d -> getAssociatedCachedDevice(d).stream()) .collect(Collectors.toSet()); // Status sequence matters here. If one of the hearing devices is in previous // ConnectionStatus, we will treat whole hearing devices is in this status. // E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED // status, the hearing devices connection status will notify CONNECTED status. if (isConnectingOrDisconnectingConnectionStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.CONNECTING_OR_DISCONNECTING; } else if (isActiveConnectionStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.ACTIVE; } else if (isConnectedStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.CONNECTED; } else if (isDisconnectedStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.DISCONNECTED; } else { mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; } if (DEBUG) { Log.d(TAG, "updateDevicesConnectionStatus: " + mDevicesConnectionStatus); } } /** * @return all the related CachedBluetoothDevices for this device. */ @NonNull public Set<CachedBluetoothDevice> getAssociatedCachedDevice( @NonNull CachedBluetoothDevice device) { ArraySet<CachedBluetoothDevice> cachedDeviceSet = new ArraySet<>(); cachedDeviceSet.add(device); // Associated device should be added into memberDevice if it support CSIP profile. Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); if (!memberDevices.isEmpty()) { cachedDeviceSet.addAll(memberDevices); return cachedDeviceSet; } // If not support CSIP profile, it should be ASHA hearing device and added into subDevice. CachedBluetoothDevice subDevice = device.getSubDevice(); if (subDevice != null) { cachedDeviceSet.add(subDevice); return cachedDeviceSet; } return cachedDeviceSet; } private boolean isConnectingOrDisconnectingConnectionStatus( Set<CachedBluetoothDevice> devices) { HearingAidProfile hearingAidProfile = mBtManager.getProfileManager().getHearingAidProfile(); HapClientProfile hapClientProfile = mBtManager.getProfileManager().getHapClientProfile(); for (CachedBluetoothDevice device : devices) { if (hearingAidProfile != null) { int status = device.getProfileConnectionState(hearingAidProfile); if (status == BluetoothProfile.STATE_DISCONNECTING || status == BluetoothProfile.STATE_CONNECTING) { return true; } } if (hapClientProfile != null) { int status = device.getProfileConnectionState(hapClientProfile); if (status == BluetoothProfile.STATE_DISCONNECTING || status == BluetoothProfile.STATE_CONNECTING) { return true; } } } return false; } private boolean isActiveConnectionStatus(Set<CachedBluetoothDevice> devices) { for (CachedBluetoothDevice device : devices) { if ((device.isActiveDevice(BluetoothProfile.HEARING_AID) && device.isConnectedProfile(BluetoothProfile.HEARING_AID)) || (device.isActiveDevice(BluetoothProfile.LE_AUDIO) && device.isConnectedProfile(BluetoothProfile.LE_AUDIO))) { return true; } } return false; } private boolean isConnectedStatus(Set<CachedBluetoothDevice> devices) { return devices.stream().anyMatch(CachedBluetoothDevice::isConnected); } private boolean isDisconnectedStatus(Set<CachedBluetoothDevice> devices) { return devices.stream().anyMatch( d -> (!d.isConnected() && d.getBondState() == BOND_BONDED)); } /** * Gets the connection status for hearing device set. Will update connection status first if * never updated. */ @ConnectionStatus public int getDevicesConnectionStatus() { if (!mInitialDevicesConnectionStatusUpdate) { updateDevicesConnectionStatus(); } return mDevicesConnectionStatus; } void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters) { HearingAidInfo info = generateHearingAidInfo(newDevice); Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +28 −12 Original line number Diff line number Diff line Loading @@ -47,12 +47,14 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; Loading Loading @@ -345,11 +347,17 @@ public class LocalBluetoothProfileManager { oldState == BluetoothProfile.STATE_CONNECTING) { Log.i(TAG, "Failed to connect " + mProfile + " device"); } final boolean isAshaProfile = getHearingAidProfile() != null && mProfile instanceof HearingAidProfile; final boolean isHapClientProfile = getHapClientProfile() != null && mProfile instanceof HapClientProfile; final boolean isLeAudioProfile = getLeAudioProfile() != null && mProfile instanceof LeAudioProfile; final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile; final boolean isCsipProfile = getCsipSetCoordinatorProfile() != null && mProfile instanceof CsipSetCoordinatorProfile; if (getHearingAidProfile() != null && mProfile instanceof HearingAidProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { if (isAshaProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { // Check if the HiSyncID has being initialized if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) { long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice()); Loading @@ -366,11 +374,6 @@ public class LocalBluetoothProfileManager { HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice); } final boolean isHapClientProfile = getHapClientProfile() != null && mProfile instanceof HapClientProfile; final boolean isLeAudioProfile = getLeAudioProfile() != null && mProfile instanceof LeAudioProfile; final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile; if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) { // Checks if both profiles are connected to the device. Hearing aid info need Loading @@ -385,9 +388,7 @@ public class LocalBluetoothProfileManager { } } if (getCsipSetCoordinatorProfile() != null && mProfile instanceof CsipSetCoordinatorProfile && newState == BluetoothProfile.STATE_CONNECTED) { if (isCsipProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { // Check if the GroupID has being initialized if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile() Loading @@ -403,6 +404,21 @@ public class LocalBluetoothProfileManager { } } // LE_AUDIO, CSIP_SET_COORDINATOR profiles will also impact the connection status // change, e.g. device need to active on LE_AUDIO to become active connection status. final Set<Integer> hearingDeviceConnectionStatusProfileId = Set.of( BluetoothProfile.HEARING_AID, BluetoothProfile.HAP_CLIENT, BluetoothProfile.LE_AUDIO, BluetoothProfile.CSIP_SET_COORDINATOR ); if (Flags.hearingDeviceSetConnectionStatusReport()) { if (hearingDeviceConnectionStatusProfileId.contains(mProfile.getProfileId())) { mDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded( cachedDevice); } } cachedDevice.onProfileStateChanged(mProfile, newState); // Dispatch profile changed after device update boolean needDispatchProfileConnectionState = true; Loading packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +72 −12 Original line number Diff line number Diff line Loading @@ -33,21 +33,37 @@ import android.bluetooth.BluetoothUuid; import android.content.Context; import android.os.Parcel; import android.os.ParcelUuid; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.util.Collection; import java.util.List; import java.util.Map; @RunWith(RobolectricTestRunner.class) public class CachedBluetoothDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private final Context mContext = ApplicationProvider.getApplicationContext(); private final static String DEVICE_NAME_1 = "TestName_1"; private final static String DEVICE_NAME_2 = "TestName_2"; private final static String DEVICE_NAME_3 = "TestName_3"; Loading Loading @@ -82,6 +98,8 @@ public class CachedBluetoothDeviceManagerTest { @Mock private HearingAidProfile mHearingAidProfile; @Mock private HapClientProfile mHapClientProfile; @Mock private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile; @Mock private BluetoothDevice mDevice1; Loading @@ -89,12 +107,11 @@ public class CachedBluetoothDeviceManagerTest { private BluetoothDevice mDevice2; @Mock private BluetoothDevice mDevice3; private HearingAidDeviceManager mHearingAidDeviceManager; private CachedBluetoothDevice mCachedDevice1; private CachedBluetoothDevice mCachedDevice2; private CachedBluetoothDevice mCachedDevice3; private CachedBluetoothDeviceManager mCachedDeviceManager; private HearingAidDeviceManager mHearingAidDeviceManager; private Context mContext; private BluetoothClass createBtClass(int deviceClass) { Parcel p = Parcel.obtain(); Loading @@ -108,8 +125,6 @@ public class CachedBluetoothDeviceManagerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); when(mDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3); Loading @@ -129,13 +144,15 @@ public class CachedBluetoothDeviceManagerTest { when(mA2dpProfile.isProfileReady()).thenReturn(true); when(mPanProfile.isProfileReady()).thenReturn(true); when(mHearingAidProfile.isProfileReady()).thenReturn(true); when(mHapClientProfile.isProfileReady()).thenReturn(true); when(mCsipSetCoordinatorProfile.isProfileReady()) .thenReturn(true); doAnswer((invocation) -> mHearingAidProfile). when(mLocalProfileManager).getHearingAidProfile(); doAnswer((invocation) -> mCsipSetCoordinatorProfile) .when(mLocalProfileManager).getCsipSetCoordinatorProfile(); mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager); mCachedDeviceManager = spy( new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager)); mCachedDevice1 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1)); mCachedDevice2 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2)); mCachedDevice3 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3)); Loading Loading @@ -621,12 +638,55 @@ public class CachedBluetoothDeviceManagerTest { public void onActiveDeviceChanged_validHiSyncId_callExpectedFunction() { doNothing().when(mHearingAidDeviceManager).onActiveDeviceChanged(any()); when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1); cachedDevice1.setHearingAidInfo( new HearingAidInfo.Builder().setHiSyncId(HISYNCID1).build()); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHearingAidDeviceManager).onActiveDeviceChanged(mCachedDevice1); } @Test @RequiresFlagsEnabled( com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICE_SET_CONNECTION_STATUS_REPORT) public void onActiveDeviceChanged_hearingDevice_callReportConnectionStatus() { when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } @Test @RequiresFlagsEnabled( com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICE_SET_CONNECTION_STATUS_REPORT) public void onDeviceUnpaired_hearingDevice_callReportConnectionStatus() { when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onDeviceUnpaired(mCachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } @Test public void notifyHearingDevicesConnectionStatusChanged_nonHearingDevice_notCallFunction() { when(mCachedDevice1.getProfiles()).thenReturn(List.of(mA2dpProfile)); mCachedDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded(mCachedDevice1); verify(mHearingAidDeviceManager, never()).notifyDevicesConnectionStatusChanged(); } @Test public void notifyHearingDevicesConnectionStatusChanged_hearingDeviceProfile_callFunction() { when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHapClientProfile)); mCachedDeviceManager.onActiveDeviceChanged(cachedDevice1); mCachedDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded(mCachedDevice1); verify(mHearingAidDeviceManager).onActiveDeviceChanged(cachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } } Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +40 −8 Original line number Diff line number Diff line Loading @@ -444,12 +444,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } /** * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device. * @deprecated use {@link #isHearingDevice() } * // TODO: b/385679160 - Target to deprecate it and replace with #isHearingDevice() */ @Deprecated public boolean isHearingAidDevice() { return mHearingAidInfo != null; } /** * @return {@code true} if {@code cachedBluetoothDevice} support any of hearing device profile. */ public boolean isHearingDevice() { return getProfiles().stream().anyMatch( p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile)); } public int getDeviceSide() { return mHearingAidInfo != null ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID; Loading Loading @@ -910,12 +921,33 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } /** * Checks if the device is connected to the specified Bluetooth profile. * * @param profile The Bluetooth profile to check. * @return {@code true} if the device is connected to the profile. */ public boolean isConnectedProfile(LocalBluetoothProfile profile) { int status = getProfileConnectionState(profile); return status == BluetoothProfile.STATE_CONNECTED; } /** * Checks if the device is connected to the Bluetooth profile with the given ID. * * @param profileId The ID of the Bluetooth profile to check. * @return {@code true} if the device is connected to the profile. */ public boolean isConnectedProfile(int profileId) { for (LocalBluetoothProfile profile : getProfiles()) { if (profile.getProfileId() == profileId) { return isConnectedProfile(profile); } } return false; } public boolean isBusy() { synchronized (mProfileLock) { for (LocalBluetoothProfile profile : mProfiles) { Loading Loading @@ -1890,13 +1922,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> == BluetoothProfile.STATE_CONNECTED; } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device */ public boolean isConnectedLeAudioHearingAidDevice() { return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device * Loading @@ -1907,6 +1932,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device */ public boolean isConnectedLeAudioHearingAidDevice() { return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device */ Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +33 −2 Original line number Diff line number Diff line Loading @@ -24,7 +24,10 @@ import android.bluetooth.le.ScanFilter; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.flags.Flags; import java.sql.Timestamp; import java.util.ArrayList; Loading @@ -46,7 +49,7 @@ public class CachedBluetoothDeviceManager { private final LocalBluetoothManager mBtManager; @VisibleForTesting final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>(); @VisibleForTesting HearingAidDeviceManager mHearingAidDeviceManager; @VisibleForTesting Loading Loading @@ -191,6 +194,20 @@ public class CachedBluetoothDeviceManager { } } /** * Notifies the connection status if device is hearing device. * * @param device The {@link CachedBluetoothDevice} need to be hearing device */ public synchronized void notifyHearingDevicesConnectionStatusChangedIfNeeded( @NonNull CachedBluetoothDevice device) { if (!device.isHearingDevice()) { return; } mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } /** * Search for existing sub device {@link CachedBluetoothDevice}. * Loading Loading @@ -388,8 +405,14 @@ public class CachedBluetoothDeviceManager { /** Handles when the device been set as active/inactive. */ public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) { if (cachedBluetoothDevice.isHearingAidDevice()) { if (cachedBluetoothDevice == null) { return; } if (cachedBluetoothDevice.isHearingDevice()) { mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice); if (Flags.hearingDeviceSetConnectionStatusReport()) { mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } } } Loading Loading @@ -421,6 +444,14 @@ public class CachedBluetoothDeviceManager { mainDevice.unpair(); mainDevice.setSubDevice(null); } // TODO: b/386121967 - Should change to use isHearingDevice but mProfile get clear here. // Need to consider where to put this logic when using isHearingDevice() if (device.isHearingAidDevice()) { if (Flags.hearingDeviceSetConnectionStatusReport()) { mHearingAidDeviceManager.notifyDevicesConnectionStatusChanged(); } } } /** Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +154 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,10 @@ */ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_BONDED; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; Loading @@ -30,15 +33,22 @@ import android.provider.Settings; import android.util.FeatureFlagUtils; import android.util.Log; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.collection.ArraySet; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices. * HearingAidDeviceManager manages the set of remote bluetooth hearing devices. */ public class HearingAidDeviceManager { private static final String TAG = "HearingAidDeviceManager"; Loading @@ -49,6 +59,10 @@ public class HearingAidDeviceManager { private final LocalBluetoothManager mBtManager; private final List<CachedBluetoothDevice> mCachedDevices; private final HearingAidAudioRoutingHelper mRoutingHelper; @ConnectionStatus private int mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; private boolean mInitialDevicesConnectionStatusUpdate = false; HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices) { mContext = context; Loading @@ -68,6 +82,145 @@ public class HearingAidDeviceManager { mRoutingHelper = routingHelper; } /** * Defines the connection status for hearing devices. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ ConnectionStatus.NO_DEVICE_BONDED, ConnectionStatus.DISCONNECTED, ConnectionStatus.CONNECTED, ConnectionStatus.CONNECTING_OR_DISCONNECTING, ConnectionStatus.ACTIVE }) public @interface ConnectionStatus { int NO_DEVICE_BONDED = -1; int DISCONNECTED = 0; int CONNECTED = 1; int CONNECTING_OR_DISCONNECTING = 2; int ACTIVE = 3; } /** * Updates the connection status of the hearing devices based on the currently bonded * hearing aid devices. */ synchronized void notifyDevicesConnectionStatusChanged() { updateDevicesConnectionStatus(); // TODO: b/357882387 - notify connection status changes for the callers } private void updateDevicesConnectionStatus() { mInitialDevicesConnectionStatusUpdate = true; // Add all hearing devices including sub and member into a set. Set<CachedBluetoothDevice> allHearingDevices = mCachedDevices.stream() .filter(d -> d.getBondState() == BluetoothDevice.BOND_BONDED && d.isHearingDevice()) .flatMap(d -> getAssociatedCachedDevice(d).stream()) .collect(Collectors.toSet()); // Status sequence matters here. If one of the hearing devices is in previous // ConnectionStatus, we will treat whole hearing devices is in this status. // E.g. One of hearing device is in CONNECTED status and another is in DISCONNECTED // status, the hearing devices connection status will notify CONNECTED status. if (isConnectingOrDisconnectingConnectionStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.CONNECTING_OR_DISCONNECTING; } else if (isActiveConnectionStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.ACTIVE; } else if (isConnectedStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.CONNECTED; } else if (isDisconnectedStatus(allHearingDevices)) { mDevicesConnectionStatus = ConnectionStatus.DISCONNECTED; } else { mDevicesConnectionStatus = ConnectionStatus.NO_DEVICE_BONDED; } if (DEBUG) { Log.d(TAG, "updateDevicesConnectionStatus: " + mDevicesConnectionStatus); } } /** * @return all the related CachedBluetoothDevices for this device. */ @NonNull public Set<CachedBluetoothDevice> getAssociatedCachedDevice( @NonNull CachedBluetoothDevice device) { ArraySet<CachedBluetoothDevice> cachedDeviceSet = new ArraySet<>(); cachedDeviceSet.add(device); // Associated device should be added into memberDevice if it support CSIP profile. Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); if (!memberDevices.isEmpty()) { cachedDeviceSet.addAll(memberDevices); return cachedDeviceSet; } // If not support CSIP profile, it should be ASHA hearing device and added into subDevice. CachedBluetoothDevice subDevice = device.getSubDevice(); if (subDevice != null) { cachedDeviceSet.add(subDevice); return cachedDeviceSet; } return cachedDeviceSet; } private boolean isConnectingOrDisconnectingConnectionStatus( Set<CachedBluetoothDevice> devices) { HearingAidProfile hearingAidProfile = mBtManager.getProfileManager().getHearingAidProfile(); HapClientProfile hapClientProfile = mBtManager.getProfileManager().getHapClientProfile(); for (CachedBluetoothDevice device : devices) { if (hearingAidProfile != null) { int status = device.getProfileConnectionState(hearingAidProfile); if (status == BluetoothProfile.STATE_DISCONNECTING || status == BluetoothProfile.STATE_CONNECTING) { return true; } } if (hapClientProfile != null) { int status = device.getProfileConnectionState(hapClientProfile); if (status == BluetoothProfile.STATE_DISCONNECTING || status == BluetoothProfile.STATE_CONNECTING) { return true; } } } return false; } private boolean isActiveConnectionStatus(Set<CachedBluetoothDevice> devices) { for (CachedBluetoothDevice device : devices) { if ((device.isActiveDevice(BluetoothProfile.HEARING_AID) && device.isConnectedProfile(BluetoothProfile.HEARING_AID)) || (device.isActiveDevice(BluetoothProfile.LE_AUDIO) && device.isConnectedProfile(BluetoothProfile.LE_AUDIO))) { return true; } } return false; } private boolean isConnectedStatus(Set<CachedBluetoothDevice> devices) { return devices.stream().anyMatch(CachedBluetoothDevice::isConnected); } private boolean isDisconnectedStatus(Set<CachedBluetoothDevice> devices) { return devices.stream().anyMatch( d -> (!d.isConnected() && d.getBondState() == BOND_BONDED)); } /** * Gets the connection status for hearing device set. Will update connection status first if * never updated. */ @ConnectionStatus public int getDevicesConnectionStatus() { if (!mInitialDevicesConnectionStatusUpdate) { updateDevicesConnectionStatus(); } return mDevicesConnectionStatus; } void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters) { HearingAidInfo info = generateHearingAidInfo(newDevice); Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +28 −12 Original line number Diff line number Diff line Loading @@ -47,12 +47,14 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.settingslib.flags.Flags; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; Loading Loading @@ -345,11 +347,17 @@ public class LocalBluetoothProfileManager { oldState == BluetoothProfile.STATE_CONNECTING) { Log.i(TAG, "Failed to connect " + mProfile + " device"); } final boolean isAshaProfile = getHearingAidProfile() != null && mProfile instanceof HearingAidProfile; final boolean isHapClientProfile = getHapClientProfile() != null && mProfile instanceof HapClientProfile; final boolean isLeAudioProfile = getLeAudioProfile() != null && mProfile instanceof LeAudioProfile; final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile; final boolean isCsipProfile = getCsipSetCoordinatorProfile() != null && mProfile instanceof CsipSetCoordinatorProfile; if (getHearingAidProfile() != null && mProfile instanceof HearingAidProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { if (isAshaProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { // Check if the HiSyncID has being initialized if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) { long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice()); Loading @@ -366,11 +374,6 @@ public class LocalBluetoothProfileManager { HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice); } final boolean isHapClientProfile = getHapClientProfile() != null && mProfile instanceof HapClientProfile; final boolean isLeAudioProfile = getLeAudioProfile() != null && mProfile instanceof LeAudioProfile; final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile; if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) { // Checks if both profiles are connected to the device. Hearing aid info need Loading @@ -385,9 +388,7 @@ public class LocalBluetoothProfileManager { } } if (getCsipSetCoordinatorProfile() != null && mProfile instanceof CsipSetCoordinatorProfile && newState == BluetoothProfile.STATE_CONNECTED) { if (isCsipProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { // Check if the GroupID has being initialized if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile() Loading @@ -403,6 +404,21 @@ public class LocalBluetoothProfileManager { } } // LE_AUDIO, CSIP_SET_COORDINATOR profiles will also impact the connection status // change, e.g. device need to active on LE_AUDIO to become active connection status. final Set<Integer> hearingDeviceConnectionStatusProfileId = Set.of( BluetoothProfile.HEARING_AID, BluetoothProfile.HAP_CLIENT, BluetoothProfile.LE_AUDIO, BluetoothProfile.CSIP_SET_COORDINATOR ); if (Flags.hearingDeviceSetConnectionStatusReport()) { if (hearingDeviceConnectionStatusProfileId.contains(mProfile.getProfileId())) { mDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded( cachedDevice); } } cachedDevice.onProfileStateChanged(mProfile, newState); // Dispatch profile changed after device update boolean needDispatchProfileConnectionState = true; Loading
packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +72 −12 Original line number Diff line number Diff line Loading @@ -33,21 +33,37 @@ import android.bluetooth.BluetoothUuid; import android.content.Context; import android.os.Parcel; import android.os.ParcelUuid; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.util.Collection; import java.util.List; import java.util.Map; @RunWith(RobolectricTestRunner.class) public class CachedBluetoothDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private final Context mContext = ApplicationProvider.getApplicationContext(); private final static String DEVICE_NAME_1 = "TestName_1"; private final static String DEVICE_NAME_2 = "TestName_2"; private final static String DEVICE_NAME_3 = "TestName_3"; Loading Loading @@ -82,6 +98,8 @@ public class CachedBluetoothDeviceManagerTest { @Mock private HearingAidProfile mHearingAidProfile; @Mock private HapClientProfile mHapClientProfile; @Mock private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile; @Mock private BluetoothDevice mDevice1; Loading @@ -89,12 +107,11 @@ public class CachedBluetoothDeviceManagerTest { private BluetoothDevice mDevice2; @Mock private BluetoothDevice mDevice3; private HearingAidDeviceManager mHearingAidDeviceManager; private CachedBluetoothDevice mCachedDevice1; private CachedBluetoothDevice mCachedDevice2; private CachedBluetoothDevice mCachedDevice3; private CachedBluetoothDeviceManager mCachedDeviceManager; private HearingAidDeviceManager mHearingAidDeviceManager; private Context mContext; private BluetoothClass createBtClass(int deviceClass) { Parcel p = Parcel.obtain(); Loading @@ -108,8 +125,6 @@ public class CachedBluetoothDeviceManagerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); when(mDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3); Loading @@ -129,13 +144,15 @@ public class CachedBluetoothDeviceManagerTest { when(mA2dpProfile.isProfileReady()).thenReturn(true); when(mPanProfile.isProfileReady()).thenReturn(true); when(mHearingAidProfile.isProfileReady()).thenReturn(true); when(mHapClientProfile.isProfileReady()).thenReturn(true); when(mCsipSetCoordinatorProfile.isProfileReady()) .thenReturn(true); doAnswer((invocation) -> mHearingAidProfile). when(mLocalProfileManager).getHearingAidProfile(); doAnswer((invocation) -> mCsipSetCoordinatorProfile) .when(mLocalProfileManager).getCsipSetCoordinatorProfile(); mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager); mCachedDeviceManager = spy( new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager)); mCachedDevice1 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1)); mCachedDevice2 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2)); mCachedDevice3 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3)); Loading Loading @@ -621,12 +638,55 @@ public class CachedBluetoothDeviceManagerTest { public void onActiveDeviceChanged_validHiSyncId_callExpectedFunction() { doNothing().when(mHearingAidDeviceManager).onActiveDeviceChanged(any()); when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1); cachedDevice1.setHearingAidInfo( new HearingAidInfo.Builder().setHiSyncId(HISYNCID1).build()); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHearingAidDeviceManager).onActiveDeviceChanged(mCachedDevice1); } @Test @RequiresFlagsEnabled( com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICE_SET_CONNECTION_STATUS_REPORT) public void onActiveDeviceChanged_hearingDevice_callReportConnectionStatus() { when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onActiveDeviceChanged(mCachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } @Test @RequiresFlagsEnabled( com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICE_SET_CONNECTION_STATUS_REPORT) public void onDeviceUnpaired_hearingDevice_callReportConnectionStatus() { when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice1.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); mCachedDeviceManager.onDeviceUnpaired(mCachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } @Test public void notifyHearingDevicesConnectionStatusChanged_nonHearingDevice_notCallFunction() { when(mCachedDevice1.getProfiles()).thenReturn(List.of(mA2dpProfile)); mCachedDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded(mCachedDevice1); verify(mHearingAidDeviceManager, never()).notifyDevicesConnectionStatusChanged(); } @Test public void notifyHearingDevicesConnectionStatusChanged_hearingDeviceProfile_callFunction() { when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHapClientProfile)); mCachedDeviceManager.onActiveDeviceChanged(cachedDevice1); mCachedDeviceManager.notifyHearingDevicesConnectionStatusChangedIfNeeded(mCachedDevice1); verify(mHearingAidDeviceManager).onActiveDeviceChanged(cachedDevice1); verify(mHearingAidDeviceManager).notifyDevicesConnectionStatusChanged(); } }