settingslib/bluetooth: Add LeAudio support

Bug: 150670922
Tag: #feature
Sponsor: jpawlowski@
Test: Manual
Merged-In: I45402a6d315d8ccc2bc756bdc9937e81eea3c58a
Change-Id: I45402a6d315d8ccc2bc756bdc9937e81eea3c58a
This commit is contained in:
Łukasz Rymanowski
2021-10-14 10:22:53 +00:00
committed by Jakub Pawlowski
parent e71f30f079
commit c0aa3b3276
6 changed files with 376 additions and 2 deletions

View File

@@ -0,0 +1,31 @@
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?android:attr/colorControlNormal" >
<path
android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
android:fillColor="#FFFFFFFF"/>
<path
android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
android:fillColor="#FFFFFFFF"/>
<path
android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
android:fillColor="#FFFFFFFF"/>
</vector>

View File

@@ -257,8 +257,12 @@
<!-- Bluetooth settings. The user-visible string that is used whenever referring to the Hearing Aid profile. -->
<string name="bluetooth_profile_hearing_aid">Hearing Aids</string>
<!-- Bluetooth settings. The user-visible string that is used whenever referring to the LE_AUDIO profile. -->
<string name="bluetooth_profile_le_audio">LE_AUDIO</string>
<!-- Bluetooth settings. Connection options screen. The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. -->
<string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string>
<!-- Bluetooth settings. Connection options screen. The summary for the LE_AUDIO checkbox preference when LE_AUDIO is connected. -->
<string name="bluetooth_le_audio_profile_summary_connected">Connected to LE_AUDIO</string>
<!-- Bluetooth settings. Connection options screen. The summary for the A2DP checkbox preference when A2DP is connected. -->
<string name="bluetooth_a2dp_profile_summary_connected">Connected to media audio</string>
@@ -299,6 +303,8 @@
<string name="bluetooth_hid_profile_summary_use_for">Use for input</string>
<!-- Bluetooth settings. Connection options screen. The summary for the Hearing Aid checkbox preference that describes how checking it will set the Hearing Aid profile as preferred. -->
<string name="bluetooth_hearing_aid_profile_summary_use_for">Use for Hearing Aids</string>
<!-- Bluetooth settings. Connection options screen. The summary for the LE_AUDIO checkbox preference that describes how checking it will set the LE_AUDIO profile as preferred. -->
<string name="bluetooth_le_audio_profile_summary_use_for">Use for LE_AUDIO</string>
<!-- Button text for accepting an incoming pairing request. [CHAR LIMIT=20] -->
<string name="bluetooth_pairing_accept">Pair</string>

View File

@@ -22,6 +22,7 @@ import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -116,6 +117,8 @@ public class BluetoothEventManager {
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
addHandler(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
// Headset state changed broadcasts
addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
@@ -455,6 +458,9 @@ public class BluetoothEventManager {
bluetoothProfile = BluetoothProfile.HEADSET;
} else if (Objects.equals(action, BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.HEARING_AID;
} else if (Objects.equals(action,
BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.LE_AUDIO;
} else {
Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
return;

View File

@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
@@ -109,10 +110,12 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private boolean mIsActiveDeviceA2dp = false;
private boolean mIsActiveDeviceHeadset = false;
private boolean mIsActiveDeviceHearingAid = false;
private boolean mIsActiveDeviceLeAudio = false;
// Media profile connect state
private boolean mIsA2dpProfileConnectedFail = false;
private boolean mIsHeadsetProfileConnectedFail = false;
private boolean mIsHearingAidProfileConnectedFail = false;
private boolean mIsLeAudioProfileConnectedFail = false;
// Group second device for Hearing Aid
private CachedBluetoothDevice mSubDevice;
// Group member devices for the coordinated set
@@ -133,6 +136,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
case BluetoothProfile.HEARING_AID:
mIsHearingAidProfileConnectedFail = true;
break;
case BluetoothProfile.LE_AUDIO:
mIsLeAudioProfileConnectedFail = true;
break;
default:
Log.w(TAG, "handleMessage(): unknown message : " + msg.what);
break;
@@ -266,6 +272,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
case BluetoothProfile.HEARING_AID:
mIsHearingAidProfileConnectedFail = isFailed;
break;
case BluetoothProfile.LE_AUDIO:
mIsLeAudioProfileConnectedFail = isFailed;
break;
default:
Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId);
break;
@@ -541,6 +550,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
result = true;
}
}
LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) {
if (leAudioProfile.setActiveDevice(getDevice())) {
Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this);
result = true;
}
}
return result;
}
@@ -619,6 +635,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
changed = (mIsActiveDeviceHearingAid != isActive);
mIsActiveDeviceHearingAid = isActive;
break;
case BluetoothProfile.LE_AUDIO:
changed = (mIsActiveDeviceLeAudio != isActive);
mIsActiveDeviceLeAudio = isActive;
break;
default:
Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
" isActive " + isActive);
@@ -650,6 +670,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return mIsActiveDeviceHeadset;
case BluetoothProfile.HEARING_AID:
return mIsActiveDeviceHearingAid;
case BluetoothProfile.LE_AUDIO:
return mIsActiveDeviceLeAudio;
default:
Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
break;
@@ -744,6 +766,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
if (hearingAidProfile != null) {
mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
}
LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
if (leAudio != null) {
mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice);
}
}
/**
@@ -984,6 +1010,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
boolean a2dpConnected = true; // A2DP is connected
boolean hfpConnected = true; // HFP is connected
boolean hearingAidConnected = true; // Hearing Aid is connected
boolean leAudioConnected = true; // LeAudio is connected
int leftBattery = -1;
int rightBattery = -1;
@@ -1015,6 +1042,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
hfpConnected = false;
} else if (profile instanceof HearingAidProfile) {
hearingAidConnected = false;
} else if (profile instanceof LeAudioProfile) {
leAudioConnected = false;
}
}
break;
@@ -1057,7 +1086,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
// 1. Hearing Aid device active.
// 2. Headset device active with in-calling state.
// 3. A2DP device active without in-calling state.
if (a2dpConnected || hfpConnected || hearingAidConnected) {
// 4. Le Audio device active
if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) {
final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
if ((mIsActiveDeviceHearingAid)
|| (mIsActiveDeviceHeadset && isOnCall)
@@ -1092,7 +1122,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private boolean isProfileConnectedFail() {
return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail
|| (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail);
|| (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail)
|| mIsLeAudioProfileConnectedFail;
}
/**
@@ -1103,6 +1134,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
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
boolean leAudioNotConnected = false; // LeAudio is preferred but not connected
synchronized (mProfileLock) {
for (LocalBluetoothProfile profile : getProfiles()) {
@@ -1128,6 +1160,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
hfpNotConnected = true;
} else if (profile instanceof HearingAidProfile) {
hearingAidNotConnected = true;
} else if (profile instanceof LeAudioProfile) {
leAudioNotConnected = true;
}
}
break;
@@ -1166,6 +1200,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
}
if (!leAudioNotConnected && mIsActiveDeviceLeAudio) {
activeDeviceString = activeDeviceStringsArray[1];
return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
}
if (profileConnected) {
if (a2dpNotConnected && hfpNotConnected) {
if (batteryLevelPercentageString != null) {
@@ -1235,6 +1274,15 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
BluetoothProfile.STATE_CONNECTED;
}
/**
* @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device
*/
public boolean isConnectedLeAudioDevice() {
LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
return leAudio != null && leAudio.getConnectionStatus(mDevice) ==
BluetoothProfile.STATE_CONNECTED;
}
private boolean isConnectedSapDevice() {
SapProfile sapProfile = mProfileManager.getSapProfile();
return sapProfile != null && sapProfile.getConnectionStatus(mDevice)

View File

@@ -0,0 +1,264 @@
/* Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
- www.ehima.com
*/
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LeAudioProfile implements LocalBluetoothProfile {
private static final String TAG = "LeAudioProfile";
private static boolean DEBUG = true;
private Context mContext;
private BluetoothLeAudio mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "LE_AUDIO";
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class LeAudioServiceListener
implements BluetoothProfile.ServiceListener {
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG,"Bluetooth service connected");
}
mService = (BluetoothLeAudio) proxy;
// We just bound to the service, so refresh the UI for any connected LeAudio devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(LeAudioProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG,"Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.LE_AUDIO;
}
LeAudioProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(
context, new LeAudioServiceListener(),
BluetoothProfile.LE_AUDIO);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/*
* @hide
*/
public boolean connect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.connect(device);
}
/*
* @hide
*/
public boolean disconnect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.disconnect(device);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
}
public List<BluetoothDevice> getActiveDevices() {
if (mService == null) {
return new ArrayList<>();
}
return mService.getActiveDevices();
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isEnabled = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isEnabled;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_le_audio;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_le_audio_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_le_audio_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return R.drawable.ic_bt_le_audio;
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.LE_AUDIO,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
}

View File

@@ -24,6 +24,7 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothMap;
@@ -102,6 +103,7 @@ public class LocalBluetoothProfileManager {
private PbapServerProfile mPbapProfile;
private HearingAidProfile mHearingAidProfile;
private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
private LeAudioProfile mLeAudioProfile;
private SapProfile mSapProfile;
private VolumeControlProfile mVolumeControlProfile;
@@ -232,6 +234,14 @@ public class LocalBluetoothProfileManager {
// Note: no event handler for VCP, only for being connectable.
mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile);
}
if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO profile");
}
mLeAudioProfile = new LeAudioProfile(mContext, mDeviceManager, this);
addProfile(mLeAudioProfile, LeAudioProfile.NAME,
BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
}
if (mCsipSetCoordinatorProfile == null
&& supportedList.contains(BluetoothProfile.CSIP_SET_COORDINATOR)) {
if (DEBUG) {
@@ -487,6 +497,10 @@ public class LocalBluetoothProfileManager {
return mHearingAidProfile;
}
public LeAudioProfile getLeAudioProfile() {
return mLeAudioProfile;
}
SapProfile getSapProfile() {
return mSapProfile;
}
@@ -614,6 +628,11 @@ public class LocalBluetoothProfileManager {
removedProfiles.remove(mHearingAidProfile);
}
if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
profiles.add(mLeAudioProfile);
removedProfiles.remove(mLeAudioProfile);
}
if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
profiles.add(mSapProfile);
removedProfiles.remove(mSapProfile);