diff --git a/core/java/android/bluetooth/BluetoothHearingAid.java b/core/java/android/bluetooth/BluetoothHearingAid.java
index 647e0d033fb7c..8f8083ed73e2c 100644
--- a/core/java/android/bluetooth/BluetoothHearingAid.java
+++ b/core/java/android/bluetooth/BluetoothHearingAid.java
@@ -17,6 +17,7 @@
package android.bluetooth;
import android.Manifest;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -378,6 +379,76 @@ public final class BluetoothHearingAid implements BluetoothProfile {
}
}
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, Hearing Aid audio
+ * streaming is to the active Hearing Aid device. If a remote device
+ * is not connected, it cannot be selected as active.
+ *
+ *
This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ *
Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+ * permission.
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()
+ && ((device == null) || isValidDevice(device))) {
+ mService.setActiveDevice(device);
+ return true;
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Check whether the device is active.
+ *
+ *
Requires {@link android.Manifest.permission#BLUETOOTH}
+ * permission.
+ *
+ * @return the connected device that is active or null if no device
+ * is active
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public boolean isActiveDevice(@Nullable BluetoothDevice device) {
+ if (VDBG) log("isActiveDevice()");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()
+ && ((device == null) || isValidDevice(device))) {
+ return mService.isActiveDevice(device);
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
/**
* Set priority of the profile
*
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 9b13d0bb8abe8..20a5afe810a8f 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -177,6 +177,8 @@
android:name="android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED" />
+
// Active device state
private boolean mIsActiveDeviceA2dp = false;
private boolean mIsActiveDeviceHeadset = false;
+ private boolean mIsActiveDeviceHearingAid = false;
/**
* Describes the current device and profile for logging.
@@ -416,6 +417,36 @@ public class CachedBluetoothDevice implements Comparable
}
}
+ /**
+ * Set this device as active device
+ * @return true if at least one profile on this device is set to active, false otherwise
+ */
+ public boolean setActive() {
+ boolean result = false;
+ A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
+ if (a2dpProfile.setActiveDevice(getDevice())) {
+ Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
+ result = true;
+ }
+ }
+ HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
+ if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
+ if (headsetProfile.setActiveDevice(getDevice())) {
+ Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
+ result = true;
+ }
+ }
+ HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
+ if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
+ if (hearingAidProfile.setActiveDevice(getDevice())) {
+ Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
+ result = true;
+ }
+ }
+ return result;
+ }
+
void refreshName() {
fetchName();
dispatchAttributesChanged();
@@ -478,6 +509,10 @@ public class CachedBluetoothDevice implements Comparable
changed = (mIsActiveDeviceHeadset != isActive);
mIsActiveDeviceHeadset = isActive;
break;
+ case BluetoothProfile.HEARING_AID:
+ changed = (mIsActiveDeviceHearingAid != isActive);
+ mIsActiveDeviceHearingAid = isActive;
+ break;
default:
Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
" isActive " + isActive);
@@ -501,6 +536,8 @@ public class CachedBluetoothDevice implements Comparable
return mIsActiveDeviceA2dp;
case BluetoothProfile.HEADSET:
return mIsActiveDeviceHeadset;
+ case BluetoothProfile.HEARING_AID:
+ return mIsActiveDeviceHearingAid;
default:
Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
break;
@@ -592,6 +629,10 @@ public class CachedBluetoothDevice implements Comparable
if (headsetProfile != null) {
mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
}
+ HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
+ if (hearingAidProfile != null) {
+ mIsActiveDeviceHearingAid = hearingAidProfile.isActiveDevice(mDevice);
+ }
}
/**
@@ -922,6 +963,7 @@ public class CachedBluetoothDevice implements Comparable
boolean profileConnected = false; // at least one profile is connected
boolean a2dpNotConnected = false; // A2DP is preferred but not connected
boolean hfpNotConnected = false; // HFP is preferred but not connected
+ boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
for (LocalBluetoothProfile profile : getProfiles()) {
int connectionStatus = getProfileConnectionState(profile);
@@ -943,6 +985,8 @@ public class CachedBluetoothDevice implements Comparable
} else if ((profile instanceof HeadsetProfile) ||
(profile instanceof HfpClientProfile)) {
hfpNotConnected = true;
+ } else if (profile instanceof HearingAidProfile) {
+ hearingAidNotConnected = true;
}
}
break;
@@ -975,6 +1019,10 @@ public class CachedBluetoothDevice implements Comparable
activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
}
}
+ if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
+ activeDeviceString = activeDeviceStringsArray[1];
+ return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
+ }
if (profileConnected) {
if (a2dpNotConnected && hfpNotConnected) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
index 8f9e4635bb4b0..920500f97223c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -134,6 +134,17 @@ public class HearingAidProfile implements LocalBluetoothProfile {
return mService.getConnectionState(device);
}
+ public boolean setActiveDevice(BluetoothDevice device) {
+ if (mService == null) return false;
+ mService.setActiveDevice(device);
+ return true;
+ }
+
+ public boolean isActiveDevice(BluetoothDevice device) {
+ if (mService == null) return false;
+ return mService.isActiveDevice(device);
+ }
+
public boolean isPreferred(BluetoothDevice device) {
if (mService == null) return false;
return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
index d6b20064823aa..2f5eead389931 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
@@ -71,6 +71,8 @@ public class CachedBluetoothDeviceManagerTest {
@Mock
private PanProfile mPanProfile;
@Mock
+ private HearingAidProfile mHearingAidProfile;
+ @Mock
private BluetoothDevice mDevice1;
@Mock
private BluetoothDevice mDevice2;
@@ -100,6 +102,7 @@ public class CachedBluetoothDeviceManagerTest {
when(mHfpProfile.isProfileReady()).thenReturn(true);
when(mA2dpProfile.isProfileReady()).thenReturn(true);
when(mPanProfile.isProfileReady()).thenReturn(true);
+ when(mHearingAidProfile.isProfileReady()).thenReturn(true);
mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager);
}
@@ -280,4 +283,63 @@ public class CachedBluetoothDeviceManagerTest {
assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
}
+
+ /**
+ * Test to verify onActiveDeviceChanged() with A2DP and Hearing Aid.
+ */
+ @Test
+ public void testOnActiveDeviceChanged_withA2dpAndHearingAid() {
+ CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mLocalAdapter,
+ mLocalProfileManager, mDevice1);
+ assertThat(cachedDevice1).isNotNull();
+ CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mLocalAdapter,
+ mLocalProfileManager, mDevice2);
+ assertThat(cachedDevice2).isNotNull();
+
+ when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mDevice2.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+
+ // Connect device1 for A2DP and HFP and device2 for Hearing Aid
+ cachedDevice1.onProfileStateChanged(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
+ cachedDevice1.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_CONNECTED);
+ cachedDevice2.onProfileStateChanged(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+
+ // Verify that both devices are connected and none is Active
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+
+ // The first device is active for A2DP and HFP
+ mCachedDeviceManager.onActiveDeviceChanged(cachedDevice1, BluetoothProfile.A2DP);
+ mCachedDeviceManager.onActiveDeviceChanged(cachedDevice1, BluetoothProfile.HEADSET);
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.A2DP)).isTrue();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEADSET)).isTrue();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+
+ // The second device is active for Hearing Aid and the first device is not active
+ mCachedDeviceManager.onActiveDeviceChanged(null, BluetoothProfile.A2DP);
+ mCachedDeviceManager.onActiveDeviceChanged(null, BluetoothProfile.HEADSET);
+ mCachedDeviceManager.onActiveDeviceChanged(cachedDevice2, BluetoothProfile.HEARING_AID);
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEARING_AID)).isTrue();
+
+ // No active device for Hearing Aid
+ mCachedDeviceManager.onActiveDeviceChanged(null, BluetoothProfile.HEARING_AID);
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice1.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.A2DP)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEADSET)).isFalse();
+ assertThat(cachedDevice2.isActiveDevice(BluetoothProfile.HEARING_AID)).isFalse();
+ }
}
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 2c91d5ab6755b..6593cbc86c9c3 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
@@ -61,6 +61,8 @@ public class CachedBluetoothDeviceTest {
@Mock
private PanProfile mPanProfile;
@Mock
+ private HearingAidProfile mHearingAidProfile;
+ @Mock
private BluetoothDevice mDevice;
private CachedBluetoothDevice mCachedDevice;
private Context mContext;
@@ -75,6 +77,7 @@ public class CachedBluetoothDeviceTest {
when(mHfpProfile.isProfileReady()).thenReturn(true);
when(mA2dpProfile.isProfileReady()).thenReturn(true);
when(mPanProfile.isProfileReady()).thenReturn(true);
+ when(mHearingAidProfile.isProfileReady()).thenReturn(true);
mCachedDevice = spy(
new CachedBluetoothDevice(mContext, mAdapter, mProfileManager, mDevice));
doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel();
@@ -208,6 +211,23 @@ public class CachedBluetoothDeviceTest {
assertThat(mCachedDevice.getConnectionSummary()).isNull();
}
+ @Test
+ public void testGetConnectionSummary_testSingleProfileActiveDeviceHearingAid() {
+ // Test without battery level
+ // Set Hearing Aid profile to be connected and test connection state summary
+ mCachedDevice.onProfileStateChanged(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+ assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Connected");
+
+ // Set device as Active for Hearing Aid and test connection state summary
+ mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
+ assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Connected, active");
+
+ // Set Hearing Aid profile to be disconnected and test connection state summary
+ mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.HEARING_AID);
+ mCachedDevice.onProfileStateChanged(mHearingAidProfile, BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(mCachedDevice.getConnectionSummary()).isNull();
+ }
+
@Test
public void testGetConnectionSummary_testMultipleProfilesActiveDevice() {
// Test without battery level
@@ -299,4 +319,24 @@ public class CachedBluetoothDeviceTest {
// Verify new alias is returned on getName
assertThat(cachedBluetoothDevice.getName()).isEqualTo(DEVICE_ALIAS_NEW);
}
+
+ @Test
+ public void testSetActive() {
+ when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+ when(mProfileManager.getHeadsetProfile()).thenReturn(mHfpProfile);
+ when(mA2dpProfile.setActiveDevice(any(BluetoothDevice.class))).thenReturn(true);
+ when(mHfpProfile.setActiveDevice(any(BluetoothDevice.class))).thenReturn(true);
+
+ assertThat(mCachedDevice.setActive()).isFalse();
+
+ mCachedDevice.onProfileStateChanged(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
+ assertThat(mCachedDevice.setActive()).isTrue();
+
+ mCachedDevice.onProfileStateChanged(mA2dpProfile, BluetoothProfile.STATE_DISCONNECTED);
+ mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_CONNECTED);
+ assertThat(mCachedDevice.setActive()).isTrue();
+
+ mCachedDevice.onProfileStateChanged(mHfpProfile, BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(mCachedDevice.setActive()).isFalse();
+ }
}