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(); + } }